Spring Mvc对自定义controller入参预处理
在初学springmvc框架时,我就一直有一个疑问,为什么controller方法上竟然可以放这么多的参数,而且都能得到想要的对象,比如HttpServletRequest或HttpServletResponse,各种注解@RequestParam、@RequestHeader、@RequestBody、@PathVariable、@ModelAttribute等。相信很多初学者都曾经感慨过。
这篇文章就是讲解处理这方面内容的
我们可以模仿springmvc的源码,实现一些我们自己的实现类,而方便我们的代码开发。
HandlerMethodArgumentResolver接口说明
1
2
3
4
5
6
7
8
9
10
11
12
|
package org.springframework.web.method.support; import org.springframework.core.MethodParameter; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; public interface HandlerMethodArgumentResolver { //用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument。 boolean supportsParameter(MethodParameter parameter); //真正用于处理参数分解的方法,返回的Object就是controller方法上的形参对象。 Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception; } |
示例
本示例显示如何 优雅地将传入的信息转化成自定义的实体传入controller方法。
post 数据:
first_name = Bill
last_name = Gates
初学者一般喜欢类似下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package com.demo.controller; import javax.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import com.demo.domain.Person; import com.demo.mvc.annotation.MultiPerson; import lombok.extern.slf4j.Slf4j; @Slf4j @Controller @RequestMapping ( "demo1" ) public class HandlerMethodArgumentResolverDemoController { @ResponseBody @RequestMapping (method = RequestMethod.POST) public String addPerson(HttpServletRequest request) { String firstName = request.getParameter( "first_name" ); String lastName = request.getParameter( "last_name" ); Person person = new Person(firstName, lastName); log.info(person.toString()); return person.toString(); } } |
这样的代码强依赖了javax.servlet-api的HttpServletRequest对象,并且把初始化Person对象这“活儿”加塞给了controller。代码显得累赘不优雅。在controller里我只想使用person而不想组装person,想要类似下面的代码:
1
2
3
4
5
|
@RequestMapping (method = RequestMethod.POST) public String addPerson(Person person) { log.info(person.toString()); return person.toString(); } |
直接在形参列表中获得person。那么这该如实现呢?
我们需要定义如下的一个参数分解器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package com.demo.mvc.component; import org.springframework.core.MethodParameter; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import com.demo.domain.Person; public class PersonArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().equals(Person. class ); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { String firstName = webRequest.getParameter( "first_name" ); String lastName = webRequest.getParameter( "last_name" ); return new Person(firstName, lastName); } } |
在supportsParameter中判断是否需要启用分解功能,这里判断形参类型是否为Person类,也就是说当形参遇到Person类时始终会执行该分解流程resolveArgument,也可以基于paramter上是否有我们指定的自定义注解判断是否需要流程分解。在resolveArgument中处理person的初始化工作。
注册自定义分解器
传统XML配置:
1
2
3
4
5
|
< mvc:annotation-driven > < mvc:argument-resolvers > < bean class = "com.demo.mvc.component.PersonArgumentResolver" /> </ mvc:argument-resolvers > </ mvc:annotation-driven > |
或
1
2
3
4
5
|
< bean class = "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" > < property name = "customArgumentResolvers" > < bean class = "com.demo.mvc.component.PersonArgumentResolver" /> </ property > </ bean > |
spring boot java代码配置:
1
2
3
4
5
6
|
public class WebConfig extends WebMvcConfigurerAdapter{ @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add( new CustomeArgumentResolver()); } } |
SpringMVC技巧之通用Controller
一个通用Controller。大多数情况下不再需要编写任何Controller层代码,将开发人员的关注点全部集中到Service层。
1. 前言
平时在进行传统的MVC开发时,为了完成某个特定的功能,我们通常需要同时编写Controller,Service,Dao层的代码。代码模式大概是这样的。
这里只贴出Controller层的代码,Service层也不是本次我们的关注点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// ----------------------------------------- Controller层 @RestController @RequestMapping ( "/a" ) public class AController { @Resource (name = "aService" ) private AService aService; @PostMapping (value = "/a" ) public ResponseBean<String> a(HttpServletRequest request, HttpServletResponse response) { final String name = WebUtils.findParameterValue(request, "name" ); return ResponseBean.of(aService.invoke(name)); } } // ----------------------------------------- 前端访问路径 // {{rootPath}}/a/a.do |
2. 问题
只要有过几个月Java Web开发经验的,应该对这样的代码非常熟悉,熟悉到恶心。我们稍微注意下就会发现:上面的Controller代码中,大致做了如下事情:
收集前端传递过来的参数。
将第一步收集来的参数传递给相应的Service层的某个方法执行。
将Service层执行后的结果使用Controller层特有的ResponseBean进行封装后返回给前台。
所以我们在排除掉少有的特殊情况之后,就会发现在一般情况下这个所谓的Controller层的存在感实在有点稀薄。因此本文尝试去除掉这部分枯燥的重复性代码。
3. 解决方案
直接上代码。talk is cheap, show me the code。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
// 这里之所以是 /lq , 而不是 /* ; 是因为 AntPathMatcher.combine 方法中进行合并时的处理, 导致 前一个 /* 丢失 /** * <p> 直接以前端传递来的Serivce名+方法名去调用Service层的同名方法; Controller层不再需要写任何代码 * <p> 例子 * <pre> * 前端: /lq/thirdService/queryTaskList.do * Service层相应的方法签名: Object queryTaskList(Map<String, Object> parameterMap) * 相应的Service注册到Spring容器中的id : thirdServiceService * </pre> * @author LQ * */ @RestController @RequestMapping ( "/lq" ) public class CommonController { private static final Logger LOG = LoggerFactory.getLogger(ThirdServiceController. class ); @PostMapping (value = "/{serviceName}/{serviceMethodName}" ) public void common( @PathVariable String serviceName, @PathVariable final String serviceMethodName, HttpServletRequest request, HttpServletResponse response) { // 收集前台传递来的参数, 并作预处理 final Map<String, String> parameterMap = HtmlUtils.getParameterMap(request); final Map<String, Object> paramsCopy = preDealOutParam(parameterMap); // 获取本次的调度服务名和相应的方法名 //final List<String> serviceAndMethod = parseServiceAndMethod(request); //final String serviceName = serviceAndMethod.get(0) + "Service"; //final String serivceMethodName = serviceAndMethod.get(1); // 直接使用Spring3.x新加入的@PathVariable注解; 代替上面的自定义操作 serviceName = serviceName + "Service" ; final String fullServiceMethodName = StringUtil.format( "{}.{}" , serviceName, serivceMethodName); // 输出日志, 方便回溯 LOG.debug( "### current request method is [ {} ] , parameters is [ {} ]" , fullServiceMethodName, parameterMap); // 获取Spring中注册的Service Bean final Object serviceBean = SpringBeanFactory.getBean(serviceName); Object rv; try { // 调用Service层的方法 rv = ReflectUtil.invoke(serviceBean, serivceMethodName, paramsCopy); // 若用户返回一个主动构建的FriendlyException if (rv instanceof FriendlyException) { rv = handlerException(fullServiceMethodName, (FriendlyException) rv); } else { rv = returnVal(rv); } } catch (Exception e) { rv = handlerException(fullServiceMethodName, e); } LOG.debug( "### current request method [ {} ] has dealed, rv is [ {} ]" , fullServiceMethodName, rv); HtmlUtils.writerJson(response, rv); } /** * 解析出Service和相应的方法名 * @param request * @return */ private List<String> parseServiceAndMethod(HttpServletRequest request) { // /lq/thirdService/queryTaskList.do 解析出 [ thirdService, queryTaskList ] final String serviceAndMethod = StringUtil.subBefore(request.getServletPath(), "." , false ); List<String> split = StringUtil.split(serviceAndMethod, '/' , true , true ); return split.subList( 1 , split.size()); } // 将传递来的JSON字符串转换为相应的Map, List等 private Map<String, Object> preDealOutParam( final Map<String, String> parameterMap) { final Map<String, Object> outParams = new HashMap<String, Object>(parameterMap.size()); for (Map.Entry<String, String> entry : parameterMap.entrySet()) { outParams.put(entry.getKey(), entry.getValue()); } for (Map.Entry<String, Object> entry : outParams.entrySet()) { final String value = (String) entry.getValue(); if (StringUtil.isEmpty(value)) { entry.setValue( "" ); continue ; } Object parsedObj = JSONUtil.tryParse(value); // 不是JSON字符串格式 if ( null == parsedObj) { continue ; } entry.setValue(parsedObj); } return outParams; } // 构建成功执行后的返回值 private Object returnVal(Object data) { return MapUtil.newMapBuilder().put( "data" , data).put( "status" , 200 ).put( "msg" , "success" ).build(); } // 构建执行失败后的返回值 private Object handlerException(String distributeMethod, Throwable e) { final String logInfo = StringUtil.format( "[ {} ] fail" , distributeMethod); LOG.error(logInfo, ExceptionUtil.getRootCause(e)); return MapUtil.newMapBuilder().put( "data" , "" ).put( "status" , 500 ) .put( "msg" , ExceptionUtil.getRootCause(e).getMessage()).build(); } } |
4. 使用
到此为止,Controller层的代码就算是完成了。之后的开发工作中,在绝大多数情况下,我们将不再需要编写任何Controller层的代码。只要遵循如下的约定,前端将会直接调取到Service层的相应方法,并获取到约定格式的响应值。
- 前端请求路径 : {{rootPath}}/lq/serviceName/serviceMethodName.do
- {{rootPath}} : 访问地址的根路径
- lq :自定义的固定名称,用于满足SpringMVC的映射规则。
- serviceName : 用于获取Spring容器中的Service Bean。这里的规则是 该名称后附加上Service字符来作为Bean Id来从Spring容器中获取相应 Service Bean。
- serviceMethodName : 第三步中找到的Service Bean中的名为serviceMethodName的方法。签名为Object serviceMethodName(Map<String,Object> param)。
5. 特殊需求
对于有额外需要的特殊Controller,可以完全按照之前的Controller层写法。没有任何额外需要注意的地方。
6. 完善
上面的Service层的方法签名中,其参数使用的是固定的Map<String,Object> param。对Map和Bean的争论由来已久,经久不衰,这里不搅和这趟浑水。
对于希望使用Bean作为方法参数的,可以参考SpringMVC中对Controller层方法调用的实现,来达到想要的效果。具体的实现就不在这里献丑了,有兴趣的同学可以参考下源码ServletInvocableHandlerMethod.invokeAndHandle。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。
原文链接:https://www.jianshu.com/p/ac976b9fd8d7