Spring控制器设计模式继承相关两大技术问题咨询
针对你遇到的这两个Spring Controller开发中的痛点,我整理了几个实用的解决方案,都是实际项目里验证过的优化方式:
一、自动填充父类变量,告别重复赋值
现在你每个子类Controller都要手动给USER、REQUEST_TYPE这些父类变量赋值,不仅重复劳动,还容易漏填。这里有两种方式可以解决:
1. 父类抽离初始化方法,统一处理赋值
最简单的方式是把重复的赋值逻辑抽到父类的一个方法里,子类只需要调用一次就行,避免漏写:
@SessionAttributes(value = {"user", "..."}) public abstract class MasterController<T> { public Model MODEL; public HashMap<String, String> INCOMING_PARAMS; public RequestType REQUEST_TYPE; public User USER; // 统一初始化方法,把所有赋值逻辑放这 protected void init(User user, HashMap<String, String> params, Model model, RequestType requestType) { this.USER = user; this.INCOMING_PARAMS = params; this.MODEL = model; this.REQUEST_TYPE = requestType; } public abstract T request( @ModelAttribute("user") User _user, @RequestParam HashMap<String, String> _params, Model _model, SessionStatus _sessionStatus, HttpServletRequest _request, HttpServletResponse _response); public String otherCommonFunction(String _a) { ... } }
子类里就简化成一行调用:
@Controller public class LoginController extends MasterController<String> { @GetMapping(value = "/account-login-page") @Override public String request( @ModelAttribute("user") User _user, @RequestParam HashMap<String, String> _params, Model _model, SessionStatus _sessionStatus, HttpServletRequest _request, HttpServletResponse _response) { // 一行搞定所有变量赋值 init(_user, _params, _model, RequestType.GET); // 你的业务逻辑 return "..."; } }
这种方式改动最小,马上就能见效,而且能保证所有变量都被正确赋值。
2. 用@ModelAttribute实现自动初始化
如果想彻底不用手动调用,可以在父类里加一个被@ModelAttribute标注的方法——这个方法会在每个请求处理方法执行前自动触发,刚好可以用来完成父类变量的填充:
@SessionAttributes(value = {"user", "..."}) public abstract class MasterController<T> { public Model MODEL; public HashMap<String, String> INCOMING_PARAMS; public RequestType REQUEST_TYPE; public User USER; // 这个方法会在每个request方法执行前自动调用 @ModelAttribute public void populateCommonAttributes(@ModelAttribute("user") User user, @RequestParam HashMap<String, String> params, Model model, HttpServletRequest request) { this.USER = user; this.INCOMING_PARAMS = params; this.MODEL = model; // 根据请求方法自动判断REQUEST_TYPE this.REQUEST_TYPE = "GET".equalsIgnoreCase(request.getMethod()) ? RequestType.GET : RequestType.POST; } public abstract T request( @ModelAttribute("user") User _user, @RequestParam HashMap<String, String> _params, Model _model, SessionStatus _sessionStatus, HttpServletRequest _request, HttpServletResponse _response); public String otherCommonFunction(String _a) { ... } }
现在子类里完全不用写赋值代码了,Spring会自动帮你把父类的变量填充好,而且每次请求都会执行,保证变量是当前请求的最新数据。
二、单个Controller多方法的参数一致性问题
单个Controller里有多个@GetMapping方法,每个都要复制一长串参数,不仅麻烦还容易出错,抽象方法又只能实现一次。这里有两个进阶方案:
1. 封装公共参数为DTO类
把所有重复的参数封装成一个自定义的RequestContext类,这样每个方法只需要处理这个DTO对象,参数列表会简洁很多:
// 封装所有公共请求参数的DTO public class RequestContext { private User user; private HashMap<String, String> params; private Model model; private SessionStatus sessionStatus; private HttpServletRequest request; private HttpServletResponse response; // 全参构造器 public RequestContext(User user, HashMap<String, String> params, Model model, SessionStatus sessionStatus, HttpServletRequest request, HttpServletResponse response) { this.user = user; this.params = params; this.model = model; this.sessionStatus = sessionStatus; this.request = request; this.response = response; } // 生成所有getter方法 public User getUser() { return user; } public HashMap<String, String> getParams() { return params; } // 其他getter... }
然后修改父类的抽象方法,用这个DTO作为参数:
@SessionAttributes(value = {"user", "..."}) public abstract class MasterController<T> { // 父类变量和初始化方法同上 // 抽象方法改为接收RequestContext public abstract T request(RequestContext context); public String otherCommonFunction(String _a) { ... } }
子类的方法就可以写成这样:
@Controller public class LoginController extends MasterController<String> { @GetMapping(value = "/account-login-page") @Override public String request(@ModelAttribute("user") User user, @RequestParam HashMap<String, String> params, Model model, SessionStatus sessionStatus, HttpServletRequest request, HttpServletResponse response) { RequestContext context = new RequestContext(user, params, model, sessionStatus, request, response); init(context.getUser(), context.getParams(), context.getModel(), RequestType.GET); // 业务逻辑 return "..."; } @GetMapping(value = "/account-forget-password") public String forgetPassword(@ModelAttribute("user") User user, @RequestParam HashMap<String, String> params, Model model, SessionStatus sessionStatus, HttpServletRequest request, HttpServletResponse response) { RequestContext context = new RequestContext(user, params, model, sessionStatus, request, response); init(context.getUser(), context.getParams(), context.getModel(), RequestType.GET); // 业务逻辑 return "..."; } }
虽然还是要写参数,但至少参数结构是统一的,以后要修改参数只需要改DTO类,不用每个方法都调整。
2. 自定义参数解析器,彻底简化参数列表
如果想完全去掉重复的参数,可以用Spring的HandlerMethodArgumentResolver自定义一个参数解析器,让Spring自动把这些公共参数封装成RequestContext对象,这样方法里只需要一个参数:
首先实现参数解析器:
public class RequestContextArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { // 只处理RequestContext类型的参数 return RequestContext.class.equals(parameter.getParameterType()); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { // 从请求中获取各个参数并封装成RequestContext User user = (User) webRequest.getAttribute("user", NativeWebRequest.SCOPE_SESSION); // 转换请求参数为HashMap<String, String>(注意getParameterMap返回的是String[],需要处理) Map<String, String[]> paramMap = webRequest.getParameterMap(); HashMap<String, String> params = new HashMap<>(); paramMap.forEach((key, value) -> params.put(key, value.length > 0 ? value[0] : "")); Model model = mavContainer.getModel(); SessionStatus sessionStatus = mavContainer.getSessionStatus(); HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); return new RequestContext(user, params, model, sessionStatus, request, response); } }
然后把这个解析器注册到Spring配置中:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(new RequestContextArgumentResolver()); } }
现在子类的方法就超级简洁了:
@Controller public class LoginController extends MasterController<String> { @GetMapping(value = "/account-login-page") @Override public String request(RequestContext context) { init(context.getUser(), context.getParams(), context.getModel(), RequestType.GET); // 直接用父类变量或者context里的参数做业务逻辑 return "..."; } @GetMapping(value = "/account-forget-password") public String forgetPassword(RequestContext context) { init(context.getUser(), context.getParams(), context.getModel(), RequestType.GET); // 业务逻辑 return "..."; } }
如果结合前面的@ModelAttribute自动初始化方法,连init都可以不用调用,直接用父类的变量就行,代码会更干净。
内容的提问来源于stack exchange,提问作者Gábor DANI

