作者归档:Rick

SpringMVC返回值处理器 ReturnValueHandler(五)

SpringMVC默认的返回值处理器有:

0 = {ModelAndViewMethodReturnValueHandler@8543} 
1 = {ModelMethodProcessor@8546} 
2 = {ViewMethodReturnValueHandler@8547} 
3 = {ResponseBodyEmitterReturnValueHandler@8548} 
4 = {StreamingResponseBodyReturnValueHandler@8549} 
5 = {HttpEntityMethodProcessor@8550} 
6 = {HttpHeadersReturnValueHandler@8551} 
7 = {CallableMethodReturnValueHandler@8552} 
8 = {DeferredResultMethodReturnValueHandler@8553} 
9 = {AsyncTaskMethodReturnValueHandler@8554} 
10 = {ServletModelAttributeMethodProcessor@8162} 
11 = {RequestResponseBodyMethodProcessor@8555} 
12 = {ViewNameMethodReturnValueHandler@8556} 
14 = {MapMethodProcessor@8557} 
14 = {ServletModelAttributeMethodProcessor@8558} 

我们最常用的就是 Controller + 返回值"String"Controller + 返回值+ @ResponseBody,现在分析一下,这两种情况是如何处理的。

Controller + 返回值”String”

@Controller
public class TestController {

    @GetMapping("index")
    public String toPage() {
        return "index";
    }
}
GET http://localhost:8080/index

会由 ViewNameMethodReturnValueHandler 去处理,将返回值放到ModeAndView的viewName中。这样视图解析器就会去找相应的视图。

Controller + 返回值+ @ResponseBody

RestController + 返回值 = Controller + 返回值+ @ResponseBody

GET http://localhost:8080/index

会由 RequestResponseBodyMethodProcessor 去处理,看下这个解析器的源码

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

    // Try even with null return value. ResponseBodyAdvice could get involved.
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

做2件事情:

  • 设置setRequestHandled(true) 告诉视图解析器,已经处理过了,不需要渲染
  • 找到合适的MessageConverter,然后将值直接写出。

RequestResponseBodyMethodProcessor 在处理的过程中,客户端可以使用注解@ControllerAdvice在write之前对值进行处理

@ControllerAdvice
public class MyAdvice implements ResponseBodyAdvice {
@Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        System.out.println("ResponseBodyAdvice: 执行 " + o);
        return o;
    }}

自定义HandlerMethodReturnValueHandler

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        handlers.add(new HandlerMethodReturnValueHandler() {

            @Override
            public boolean supportsReturnType(MethodParameter returnType) {
                return Integer.class == returnType.getParameterType();
            }

            @Override
            public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
                Integer value = (Integer) returnValue;
                mavContainer.addAttribute("intValue", value);
                if (value > 10) {
                    mavContainer.setViewName("big");
                } else {
                    mavContainer.setViewName("small");
                }
            }
        });
    }
}

自定义的HandlerMethodReturnValueHandler,做了2件事。

  • 将值放入Model中
  • 如果大于10跳转big.html,否则跳转small.html
@GetMapping("int/{intValue}")
public int toIntPage(@PathVariable Integer intValue) {
    return intValue;
}
GET http://localhost:8080/int/11

big.html

<body>
big.html
<h1 th:text="${intValue}"></h1>
</body>

SpringMVC自定义RequestMappingHandlerMapping实现接口的版本控制(四)

说明

在Spring MVC项目中,如果要进行restful接口的版本控制一般有以下几个方向:

  • 基于参数的版本控制
  • 基于header的版本控制
  • 基于path的版本控制

在spring MVC下,url映射到哪个method是由RequestMappingHandlerMapping来控制的,那么我们可以通过对RequestMappingHandlerMapping 定义条件来做版本控制

环境搭建

ApiVersion 表示请求的版本

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiVersion {
    String value() default "";
}

WebMvcRegistrationsConfig.java

@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {

    private static final String VERSION_PARAM_NAME = "version";

    private static final String HEADER_VERSION = "X-VERSION";

    /**
     * RequestMappingHandlerMapping 被 VersionRequestMappingHandlerMappingHandlerMapping 替换
     * 按照VersionRequestMappingHandlerMappingHandlerMapping映射逻辑进行映射
     * @return
     */
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new VersionRequestMappingHandlerMapping();
    }

    private class VersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

        @Override
        protected RequestCondition<?> getCustomMethodCondition(Method method) {
            ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
            if (apiVersion == null) {
                return null;
            }

            return new RequestCondition() {

                @Override
                public Object combine(Object o) {
                    return null;
                }

                @Override
                public Object getMatchingCondition(HttpServletRequest request) {
                    String version = resolveVersion(method, request);

                    if (Objects.equals(version, apiVersion.value())) {
                        return this;
                    }

                    return null;
                }

                @Override
                public int compareTo(Object o, HttpServletRequest request) {
                    return 0;
                }
            };
        }

        private String resolveVersion(Method method, HttpServletRequest request) {
            String version = request.getParameter(VERSION_PARAM_NAME);
            if (StringUtils.hasText(version)) {
                return version;
            }
            version = request.getHeader(HEADER_VERSION);
            if (StringUtils.hasText(version)) {
                return version;
            }
            return resolvePathVersion(method, request);
        }

        private String resolvePathVersion(Method method, HttpServletRequest request) {
            String bastPath = null;
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation.annotationType() == RequestMapping.class) {
                    bastPath = ((RequestMapping)annotation).value()[0];
                    break;
                } else if (annotation.annotationType() == GetMapping.class) {
                    bastPath = ((GetMapping)annotation).value()[0];
                    break;
                }  else if (annotation.annotationType() == PostMapping.class) {
                    bastPath = ((PostMapping)annotation).value()[0];
                    break;
                } else if (annotation.annotationType() == DeleteMapping.class) {
                    bastPath = ((DeleteMapping)annotation).value()[0];
                    break;
                } else if (annotation.annotationType() == PutMapping.class) {
                    bastPath = ((PutMapping)annotation).value()[0];
                    break;
                }
            }

            Map<String, String> uriVariables = getPathMatcher().extractUriTemplateVariables(bastPath.startsWith("/") ? bastPath : "/" + bastPath,
                    request.getServletPath());
            return uriVariables.get(VERSION_PARAM_NAME);
        }
    }
}

测试

@GetMapping("{version}/test")
public String v1(@PathVariable String version) {
    return "default";
}

@GetMapping("{version}/test")
@ApiVersion("v2")
public String v2() {
    return "v2";
}

@GetMapping("{version}/test")
@ApiVersion("v3")
public String v3() {
    return "v3";
}

/**
 * version在不一定非要在第一个位置
 * @return
 */
@GetMapping("/test/{version}")
@ApiVersion("v4")
public String v4() {
    return"v4";
}

@GetMapping("/test/{version}")
public String v5() {
    return "default";
}
curl -X GET \
  'http://localhost:8080/v1/test?version=v3' \
  -H 'X-VERSION: v2'

优先级依次是:request参数 > header > path参数

path参数,变量不一定写在第一个,只要变量名称是${version}就可以
GitHub:https://github.com/jkxyx205/spring-boot-learn/tree/master/spring-mvc-mapping

SpringMVC参数解析ArgumentResolver(三)

测试

准备测试环境

SimpleController.java

@RestController
public class SimpleController {

    @ModelAttribute("nickname")
    public String init(Model model) {
        System.out.println("Controller: SimpleController init 执行");
        model.addAttribute("postCode", "225431");
        return "Rick";
    }

    @GetMapping("request")
    public Map<String, Object> request(@Validated User user,
                                   BindingResult bindingResult, //必须紧跟@Valid/@Validated对象后面
                                   @RequestParam(required = false) @Validated @Min(18) Integer age,
                                   @RequestParam Map<String, Object> requestParamMap,
                                   @RequestParam("names") List<String> namesList,
                                   String[] names,
                                   @ModelAttribute("postCode") String postCode,
                                   @ModelAttribute("nickname") String nickname,
                                   Map<String, Object> modelMap,
                                   Model model,
                                   HttpServletRequest request) {
        System.out.println("Controller: request 执行");

        Map<String, Object> resultMap = new HashMap<>();

        resultMap.put("requestParamMap", requestParamMap);
        resultMap.put("namesList", namesList);
        resultMap.put("names", names);

        resultMap.put("user", user);

        resultMap.put("age", age);
        resultMap.put("postCode", postCode);
        resultMap.put("nickname", nickname);
        resultMap.put("bindingResult", bindingResult.getAllErrors());

        modelMap.put("hobby", "swimming");
        model.addAttribute("sex", "男");
        request.setAttribute("home", "TaiXing");
        return resultMap;
    }
}
  • 请求
http://localhost:8080/request?age=1&names=Jim,Tom&name=Rick
  • 返回
{
    "names": ["Jim", "Tom"],
    "bindingResult": [],
    "namesList": ["Jim", "Tom"],
    "nickname": "Rick",
    "postCode": "225431",
    "requestParamMap": {
        "age": "1",
        "names": "Jim,Tom",
        "name": "Rick"
    },
    "user": {
        "age": 1,
        "name": "Rick"
    },
    "age": 1
}

分析

  • @ModelAttribute 写在方法上,返回值就是model的value,init在每次请求都会执行。init中有2个model属性 nickname postCode ,值分别是“Rick”“225432”
  • 对象 User 前面加了@Valid/@Validated 解析器 ModelAttributeMethodProcessor 会进行字段的验证。如果要收集异常,那么后面紧跟BindingResult。如果没有紧跟,那么如果验证失败将会抛出异常 BindException
    源代码查看ModelAttributeMethodProcessor => validateIfApplicable(binder, parameter)
    => Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
  • age 使用 RequestParamMethodArgumentResolver 解析,解析后对字段类型由String转换成Integer,该解析器不支持验证,所以虽然加了“@Validated @Min(18)”但是并不会验证
  • requestParamMap 将所有的请求参数值放到requestParamMap中,如果参数是数组,只放第一个
  • namesList 和 names 是“简单类型” 由RequestParamMethodArgumentResolver 解析,解析后对字段类型由String转换成对应的类型
  • postCodenickname 参数使用注解 @ModelAttribute 表示从model中获取值,这里@ModelAttribute的属性name不要省略
  • model 使用解析器 ModelMethodProcessormodelMap 使用解析器 MapMethodProcessormodelMapmodel 是同一个引用。 视图解析器比如ThymeleafView会将model中的值放入request scope中
  • request 使用 ServletRequestMethodArgumentResolver 进行解析

自定义参数解析器

系统提供了很多默认的参数处理器

0 = {RequestParamMethodArgumentResolver@8604} 
1 = {RequestParamMapMethodArgumentResolver@8605} 
2 = {PathVariableMethodArgumentResolver@8606} 
3 = {PathVariableMapMethodArgumentResolver@8607} 
4 = {MatrixVariableMethodArgumentResolver@8608} 
5 = {MatrixVariableMapMethodArgumentResolver@8609} 
6 = {ServletModelAttributeMethodProcessor@8610} 
7 = {RequestResponseBodyMethodProcessor@8611} 
8 = {RequestPartMethodArgumentResolver@8612} 
9 = {RequestHeaderMethodArgumentResolver@8613} 
10 = {RequestHeaderMapMethodArgumentResolver@8614} 
11 = {ServletCookieValueMethodArgumentResolver@8615} 
12 = {ExpressionValueMethodArgumentResolver@8616} 
13 = {SessionAttributeMethodArgumentResolver@8617} 
14 = {RequestAttributeMethodArgumentResolver@8618} 
15 = {ServletRequestMethodArgumentResolver@8619} 
16 = {ServletResponseMethodArgumentResolver@8620} 
17 = {HttpEntityMethodProcessor@8621} 
18 = {RedirectAttributesMethodArgumentResolver@8622} 
19 = {ModelMethodProcessor@8623} 
20 = {MapMethodProcessor@8624} 
21 = {ErrorsMethodArgumentResolver@8625} 
22 = {SessionStatusMethodArgumentResolver@8626} 
23 = {UriComponentsBuilderMethodArgumentResolver@8627} 
24 = {PrincipalMethodArgumentResolver@8629} 
25 = {RequestParamMethodArgumentResolver@8630} 
26 = {ServletModelAttributeMethodProcessor@8631} 

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new HandlerMethodArgumentResolver() {

            @Override
            public boolean supportsParameter(MethodParameter parameter) {
                return User.class == parameter.getParameterType();
            }

            @Override
            public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
                User user = new User();
                user.setName("Green");
                user.setAge(92);
                return user;
            }
        });
    }
}

这个参数解析器会在 ServletModelAttributeMethodProcessor 之前执行,非简单对象会先使用自定义的解析器解析。自定义的参数解析器的应用场景,我们可以根据token解析出用户信息放入参数中

GitHub:https://github.com/jkxyx205/spring-boot-learn/tree/master/spring-mvc-argument-resolver

SpringMVC添加拦截器Interceptor(二)

如何添加Interceptor

实现接口 WebMvcConfigurer 并重写方法 addInterceptors

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
                System.out.println("Interceptor: HandlerInterceptor preHandle 执行目标方法是:" + handler);
                return true;
            }

            @Override
            public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
                System.out.println("Interceptor: HandlerInterceptor postHandle 执行,视图名称是:" + modelAndView);
            }

            @Override
            public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
                System.out.println("Interceptor: HandlerInterceptor afterCompletion 执行,查看异常:" + ex);
            }
        });
    }
}
  • preHandle 在目标方法执行之前调用,可以对目标方法进行拦截,阻止继续执行
  • postHandle 在目标方法执行之后,视图渲染之前执行,可以对 ModelAndView 进行获取或操作
  • afterCompletion 在视图渲染之后执行,可以获取异常
Interceptor: HandlerInterceptor preHandle 执行目标方法是:com.rick.spring.mvc.InterceptorTestApplication#index()
Interceptor: HandlerInterceptor postHandle 执行,视图名称是:ModelAndView [view="index"; model={}]
Interceptor: HandlerInterceptor afterCompletion 执行,查看异常:null

github地址:https://github.com/jkxyx205/spring-boot-learn/tree/master/spring-mvc-inteceptor

SpringMVC添加过滤器Filter(一)

实现方式

第一种方式

继承自 OncePerRequestFilter 并标记 @Component 组件

@Component
public class MyFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("Filter: MyFilter 执行之前");
        chain.doFilter(request, response);
        System.out.println("Filter: MyFilter 执行之后");
    }

}

第二种方式

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean filterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
                System.out.println("Filter: filterRegistration 执行 执行之前");
                chain.doFilter(request, response);
                System.out.println("Filter: filterRegistration 执行 执行之后");
            }
        });
        registration.addUrlPatterns("/*");
        registration.addInitParameter("paramName", "paramValue");
        registration.setName("filterRegistration");
        registration.setOrder(Integer.MIN_VALUE);
        return registration;
    }
}

这种方式可以对 Filter 做更详细的设置,如拦截的url、初始化参数、拦截器order

用途

拦截请求,可以对请求做「权限验证」,比如验证码校验。

github地址: https://github.com/jkxyx205/spring-boot-learn/tree/master/spring-mvc-filter