SpringBoot实现Rest风格方式及其源码分析
请求映射就是我们的@RequestMapping
我们以前使用如下风格表示请求
- /getUser表示获取用户
- /deleteUser表示删除用户
- /editUser表示修改用户
- /saveUser表示保存用户
而我们现在则使用Rest风格的请求方式
都是用/user这个请求,然后使用不同的请求方式表示请求不同的资源
- GET表示获取用户
- DELETE表示删除用户
- PUT表示修改用户
- POST表示保存用户
下面是我们的测试代码
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-张三";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}
但是这里有一个问题,就是我们的表单只能提交get和post的方式,并没有put和delete的方式,那我们怎么才能访问到呢?
1、SpringMVC实现Rest风格
我们再springMVC中的解决方案是,使用一个过滤器叫做HiddenHttpMethodFilter,通过使用一个隐藏域的input来实现,name必须为_method,value就是请求方式
当然我们在使用这个之前需要配置过滤器
2、SpringBoot实现Rest风格
springboot实现Rest风格的方式很简单,只需要在配置文件中配置这个属性即可
然后注意我们的表单只有是post请求,springboot才能将请求转换为对应的请求
我们测试访问
访问成功
我们来查看源码分析这个配置实现的原理
Rest映射源码分析
首先我们还是看我们的web场景的自动配置组件的核心类
就是这个类WebMvcConfiguration。
我们能看到有一个方法是有关HiddenHttpMethodFilter的
然后我们点到这个类的源码查看
这个类继承了我们的HiddenHttpMethodFilter这个类,这个类正式我们springmvc实现时使用的那个过滤器类。
然后我们查看这个HiddenHttpMethodFilter类
可以看到默认的method的参数就是
然后我们再看它的doFilterInternal方法
我们能够看到
当请求为post的时候并且这个请求灭有什么错误的时候才会进行请求方式的转化
这就是为什么我们需要将请求方式设置为post并且name为_method才能识别
至于为什么需要配置这个属性,是因为这个组件时按条件装配的
第一个条件是当容器中没有这个组件的时候自动装配
第二个条件是前缀为spring.mvc.hiddenmethod.filter 并且name属性为enabled,这个enabled的默认值为false。所以默认虽然有这个组件但是并没有启动。所以我们需要设置这个属性
3、Rest映射原理(表单提交使用Rest风格)
首先表单提交会带上_method=PUT
然后发送请求后会被HiddenHttpFilter拦截:然后判断请求是否是post请求,并且判断请求是否正常,满足条件之后,获取到key为_method的请求参数的值,这个值就是我们隐藏的input想要真正提交的请求方式
然后判断这个请求的参数是否有值,如果有值的话,则无论你的这个值是大写还是小写全部转换为大写。然后接着判断他们允许的请求方式中包不包含你的这个请求参数的值,springboot兼容put、delete、patch这三个请求。然后使用了一个包装模式,使用了一个包装模式的requestWrapper重写了原生的request的接口,覆盖了getMethod方法,然后将我们传进来的值覆盖掉原来的值,然后放行的时候放行的是wrapper包装了的request对象
这个原理是根据拦截的源码分析的
包装类的wrapper源码如下
Rest使用客户端工具就不是这样了。比如使用postman发请求,这是因为表单只能写get和post这两种请求
Rest风格还能使用这些注解写
- @PostMapping
- @GetMapping
- @PutMapping
- @DeleteMapping
这个和上面的写法的作用是相同的
扩展
我们在表单提交的时候可不可以不使用_method而是使用自定义的name
解决方案
因为设置的这个能识别的input的name默认为_method,这时因为他帮我们创建这个HiddenHttpMethodFilter这个组件时默认使用的就是它
我们如果想要自定义这个可被识别的name的话,那么我们只需要自定义一个HiddenHttpMethodFilter组件即可
通过setMethodParame这个方法来设置我们自定义的识别的name即可
请求映射源码分析
首先我们先分析DispatcherServlet这个类。这个类本质上是一个servlet
我们可以使用idea来看到它的继承关系
因为它本质上是一个servlet,那么一定覆盖重写了doGet或者doPost方法
按理说它上面继承了HttpServlet的时候这个HttpServletBean应该覆盖重写doGet和doPost方法,但是我们经过查看,它并没有覆盖重写这两个方法
而是HttpServletBean的子类FrameworkServlet覆盖重写了doGet和doPost方法
我们发现无论是调用doGet还是doPost他都会使用这么一个方法processRequest(request, response);
然后我们跟入到这个方法看看他都干了些什么
然后看到一个doService方法,我们跟进去看这个doService方法是什么
发现是个本类的抽象方法
然后我们去看它的子类,如何实现的这个方法,也就是DispatcherServlet这个类
有点多直接复制源码,不截图了
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
RequestPath previousRequestPath = null;
if (this.parseRequestPath) {
previousRequestPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
ServletRequestPathUtils.parseAndCache(request);
}
try {
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
}
}
这里面的get呀set什么的方法都是初始化一些东西,然后我们往下看看到了一个doDIspatch方法,其实这个方法才是我们最终要研究的方法
每一个请求进来之后会去走doGet或者doPost方法,然后发现doGet或者doPost方法中是一个processRequest方法,而这个方法是抽象方法,它的子类实现了之后执行doService方法,然后再doService方法中执行doDispatch方法
分析doDispatch方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
使用debug发下
执行图中这一行请求后handler直接锁定了我们的要执行的对应的controller,所以我们给这个方法打一个断点进行查看,查看他是怎么知道我们要请求的是哪一个handler
然后进入到这个断点的方法
发现有一个handlerMapping
而且这个handlerMapper有5个
然后我们看看都是哪五个
这里面的WelcomePageHanderMapping这个是我们之前说过的
我们要关注的自然是这个
其实,在getHandler这个方法中,它会使用增强for循环来遍历这5个HandlerMapping,然后分别看这个五个HandlerMapping哪一个可以处理我们访问的这个请求
它就是使用这个方法来判断我们的请求到底映射的是哪一个controller方法
其实是因为这个RequestHandlerMapping中包含了我们每一个controller和controler对应的请求,我们就可以根据这个请求找到这个对应的controller