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