SpringMVC對自定義controller入?yún)㈩A處理方式
Spring Mvc對自定義controller入?yún)㈩A處理
在初學springmvc框架時,我就一直有一個疑問,為什么controller方法上竟然可以放這么多的參數(shù),而且都能得到想要的對象,比如HttpServletRequest或HttpServletResponse,各種注解@RequestParam、@RequestHeader、@RequestBody、@PathVariable、@ModelAttribute等。相信很多初學者都曾經(jīng)感慨過。
這篇文章就是講解處理這方面內(nèi)容的
我們可以模仿springmvc的源碼,實現(xiàn)一些我們自己的實現(xiàn)類,而方便我們的代碼開發(fā)。
HandlerMethodArgumentResolver接口說明
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 { //用于判定是否需要處理該參數(shù)分解,返回true為需要,并會去調(diào)用下面的方法resolveArgument。 boolean supportsParameter(MethodParameter parameter); //真正用于處理參數(shù)分解的方法,返回的Object就是controller方法上的形參對象。 Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception; }
示例
本示例顯示如何 優(yōu)雅地將傳入的信息轉(zhuǎn)化成自定義的實體傳入controller方法。
post 數(shù)據(jù):
first_name = Bill
last_name = Gates
初學者一般喜歡類似下面的代碼
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。代碼顯得累贅不優(yōu)雅。在controller里我只想使用person而不想組裝person,想要類似下面的代碼:
@RequestMapping(method = RequestMethod.POST) public String addPerson(Person person) { log.info(person.toString()); return person.toString(); }
直接在形參列表中獲得person。那么這該如實現(xiàn)呢?
我們需要定義如下的一個參數(shù)分解器
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類時始終會執(zhí)行該分解流程resolveArgument,也可以基于paramter上是否有我們指定的自定義注解判斷是否需要流程分解。在resolveArgument中處理person的初始化工作。
注冊自定義分解器
傳統(tǒng)XML配置:
<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="com.demo.mvc.component.PersonArgumentResolver"/> </mvc:argument-resolvers> </mvc:annotation-driven>
或
<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代碼配置:
public class WebConfig extends WebMvcConfigurerAdapter{ @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(new CustomeArgumentResolver()); } }
SpringMVC技巧之通用Controller
一個通用Controller。大多數(shù)情況下不再需要編寫任何Controller層代碼,將開發(fā)人員的關注點全部集中到Service層。
1. 前言
平時在進行傳統(tǒng)的MVC開發(fā)時,為了完成某個特定的功能,我們通常需要同時編寫Controller,Service,Dao層的代碼。代碼模式大概是這樣的。
這里只貼出Controller層的代碼,Service層也不是本次我們的關注點。
// ----------------------------------------- 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開發(fā)經(jīng)驗的,應該對這樣的代碼非常熟悉,熟悉到惡心。我們稍微注意下就會發(fā)現(xiàn):上面的Controller代碼中,大致做了如下事情:
收集前端傳遞過來的參數(shù)。
將第一步收集來的參數(shù)傳遞給相應的Service層的某個方法執(zhí)行。
將Service層執(zhí)行后的結(jié)果使用Controller層特有的ResponseBean進行封裝后返回給前臺。
所以我們在排除掉少有的特殊情況之后,就會發(fā)現(xiàn)在一般情況下這個所謂的Controller層的存在感實在有點稀薄。因此本文嘗試去除掉這部分枯燥的重復性代碼。
3. 解決方案
直接上代碼。talk is cheap, show me the code。
// 這里之所以是 /lq , 而不是 /* ; 是因為 AntPathMatcher.combine 方法中進行合并時的處理, 導致 前一個 /* 丟失 /** * <p> 直接以前端傳遞來的Serivce名+方法名去調(diào)用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) { // 收集前臺傳遞來的參數(shù), 并作預處理 final Map<String, String> parameterMap = HtmlUtils.getParameterMap(request); final Map<String, Object> paramsCopy = preDealOutParam(parameterMap); // 獲取本次的調(diào)度服務名和相應的方法名 //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 { // 調(diào)用Service層的方法 rv = ReflectUtil.invoke(serviceBean, serivceMethodName, paramsCopy); // 若用戶返回一個主動構(gòu)建的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字符串轉(zhuǎn)換為相應的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; } // 構(gòu)建成功執(zhí)行后的返回值 private Object returnVal(Object data) { return MapUtil.newMapBuilder().put("data", data).put("status", 200).put("msg", "success").build(); } // 構(gòu)建執(zhí)行失敗后的返回值 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層的代碼就算是完成了。之后的開發(fā)工作中,在絕大多數(shù)情況下,我們將不再需要編寫任何Controller層的代碼。只要遵循如下的約定,前端將會直接調(diào)取到Service層的相應方法,并獲取到約定格式的響應值。
- 前端請求路徑 : {{rootPath}}/lq/serviceName/serviceMethodName.do
- {{rootPath}} : 訪問地址的根路徑
- lq :自定義的固定名稱,用于滿足SpringMVC的映射規(guī)則。
- serviceName : 用于獲取Spring容器中的Service Bean。這里的規(guī)則是 該名稱后附加上Service字符來作為Bean Id來從Spring容器中獲取相應 Service Bean。
- serviceMethodName : 第三步中找到的Service Bean中的名為serviceMethodName的方法。簽名為Object serviceMethodName(Map<String,Object> param)。
5. 特殊需求
對于有額外需要的特殊Controller,可以完全按照之前的Controller層寫法。沒有任何額外需要注意的地方。
6. 完善
上面的Service層的方法簽名中,其參數(shù)使用的是固定的Map<String,Object> param。對Map和Bean的爭論由來已久,經(jīng)久不衰,這里不攪和這趟渾水。
對于希望使用Bean作為方法參數(shù)的,可以參考SpringMVC中對Controller層方法調(diào)用的實現(xiàn),來達到想要的效果。具體的實現(xiàn)就不在這里獻丑了,有興趣的同學可以參考下源碼ServletInvocableHandlerMethod.invokeAndHandle。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Mybatis中resultMap標簽和sql標簽的設置方式
這篇文章主要介紹了Mybatis中resultMap標簽和sql標簽的設置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01