Preface
此篇大部分是对Spring MVC的一个回顾以及JSR303中bean validation规范的学习
Spring MVC 相关
Spring MVC 流程
1、 用户发送请求至前端控制器DispatcherServlet
.
2、 DispatcherServlet
收到请求调用HandlerMapping
处理器映射器.
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找), 生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet
.
4、 DispatcherServlet
调用HandlerAdapter
处理器适配器.
5、 HandlerAdapter
经过适配调用具体的处理器(Controller
, 也叫后端控制器).
6、 Controller
执行完成返回ModelAndView
.
7、 HandlerAdapter
将controller
执行结果ModelAndView
返回给DispatcherServlet
.
8、 DispatcherServlet
将ModelAndView
传给ViewReslover
视图解析器.
9、 ViewReslover
解析后返回具体View
.
10、DispatcherServlet
根据View
进行渲染视图(即将模型数据填充至视图中).
11、 DispatcherServlet
响应用户.
更多源码解析请参考: 【深入浅出spring】Spring MVC 流程解析
Spring MVC集成FastJson
https://github.com/alibaba/fastjson/wiki/%E5%9C%A8-Spring-%E4%B8%AD%E9%9B%86%E6%88%90-Fastjson
1 | <dependency> |
1 |
|
注意:
- SpringBoot 2.0.1版本中加载
WebMvcConfigurer
的顺序发生了变动, 故需使用converters.add(0, converter);
指定FastJsonHttpMessageConverter
在converters内的顺序, 否则在SpringBoot 2.0.1及之后的版本中将优先使用Jackson处理。详情:WebMvcConfigurer is overridden by WebMvcAutoConfiguration #12389 - 在
FastJsonHttpMessageConverter
之前插入一个StringHttpMessageConverter
是为了在Controller层返回String类型不会再次被FastJson序列化.
FastJson枚举映射
实现 ObjectSerializer,
以及 ObjectDeserializer
:
1 | public class EnumCodec implements ObjectSerializer, ObjectDeserializer { |
方式一: 字段上加注解
在枚举字段上添加注解:
1 | .class, deserializeUsing = EnumCodec.class) (serializeUsing = EnumCodec |
方式二: 类上加注解
1 | true, serializer = EnumCodec.class, deserializer = EnumCodec.class) (serializeEnumAsJavaBean = |
WebFlux
上面针对的是Web MVC, 对于Webflux目前不支持这种方式.
Spring Boot JSON (Date类型入参、格式化, 以及如何处理null)
1 | spring: |
- 时间格式可以在实体上使用该注解:
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
- 忽略null属性可以在实体上使用:
@JsonInclude(JsonInclude.Include.NON_NULL)
Spring Boot MVC特性
Spring boot 在spring默认基础上, 自动配置添加了以下特性
- 包含了
ContentNegotiatingViewResolver
和BeanNameViewResolver
beans. - 对静态资源的支持, 包括对WebJars的支持.
- 自动注册
Converter
,GenericConverter
,Formatter
beans. - 对
HttpMessageConverters
的支持. - 自动注册
MessageCodeResolver
. - 对静态
index.html
的支持. - 对自定义
Favicon
的支持. - 主动使用
ConfigurableWebBindingInitializer
bean
@RequestBody与@ModelAttribute
@RequestBody
: 用于接收http请求中body的字符串信息, 可在直接接收转换到Pojo.
@ModelAttribute
: 用于直接接受url?
后面的参数 如url?id=123&name=456
, 可在直接接收转换到Pojo.
模板引擎的选择
FreeMarker
Thymeleaf
Velocity
(1.4版本之后弃用, Spring Framework 4.3版本之后弃用)Groovy
Mustache
注: jsp应该尽量避免使用, 原因如下:
- jsp只能打包为: war格式, 不支持jar格式, 只能在标准的容器里面跑(tomcat, jetty都可以)
- 内嵌的Jetty目前不支持JSP
- Undertow不支持jsp
- jsp自定义错误页面不能覆盖spring boot 默认的错误页面
开启GZIP算法压缩响应流
1 | server: |
全局异常处理
在Spring Boot 2.X 中, 对于MVC抛出的异常, 默认会映射到 /error
:
参考: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-error-handling
由于默认情况下, Spring MVC 将报错转发到 /error
接口, 所以对应的Spring中也会有默认的异常处理类 BasicErrorController
:
添加自定义的错误页面
html
静态页面: 在resources/public/error/
下定义. 如添加404页面:resources/public/error/404.html
页面, 中文注意页面编码- 模板引擎页面: 在
templates/error/
下定义. 如添加5xx页面:templates/error/5xx.ftl
注:
templates/error/
这个的优先级比较resources/public/error/
高
通过@ControllerAdvice
1 | 4j |
@RestControllerAdvice
可用于返回JSON格式报文.
或者继承ResponseEntityExceptionHandler
更灵活地控制状态码、Header
等信息:
1 |
|
更多方式请看: http://www.baeldung.com/exception-handling-for-rest-with-spring
异常处理性能优化
Java 异常对象的构造是十分耗时的, 原因是创建异常对象时会调用父类 Throwable
的 fillInStackTrace()
方法生成栈追踪信息, 对于一般的业务异常, 我们可以适当优化, 先看一下 RuntimeException
的构造器:
1 | protected RuntimeException(String message, Throwable cause, |
这几个参数的意义如下:
message
异常的描述信息, 也就是在打印栈追踪信息时异常类名后面紧跟着的描述字符串cause
导致此异常发生的父异常, 即追踪信息里的caused by
enableSuppress
关于异常挂起的参数, 这里我们永远设为false
即可writableStackTrace
表示是否生成栈追踪信息, 只要将此参数设为false
, 则在构造异常对象时就不会调用 fillInStackTrace()
业务异常可以这样定义:
1 | public class XXXException extends RuntimeException { |
一般情况用第一个构造参数, 比较轻量级, 想要精准跟踪异常可以使用第三个构造参数.
404处理
Spring Boot 2.X 中会有一个Resouce的Mapping来处理静态资源, 当输入一个不存在的请求时, 总会匹配到这个Mapping:
此时的404错误是 ResourceHttpRequestHandler#handleRequest
中因为找不到resource从而调用response#sendError
发出的:
一般地如果是前后分离的项目, 都不要将资源放在后端, 所以可以用过以下配置关闭这个万能的Mapping:
1 | spring: |
通过以上配置后, 将加载不了静态资源, 如果需要加载, 需要自定义配置, 比如Swagger:
1 |
|
如果需要通过抛异常的方式捕获404这个异常, 需要通过以下配置:
1 | spring: |
之后可以通过 @ExceptionHandler(value = NoHandlerFoundException.class)
处理这个404了, 而不是转发到 /error
.
静态资源
设置静态资源放到指定路径下
1 | spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/static/ |
自定义消息转化器
1 |
|
自定义SpringMVC的拦截器
有些时候我们需要自己配置SpringMVC而不是采用默认, 比如增加一个拦截器
1 | public class MyInterceptor implements HandlerInterceptor { |
1 |
|
或者可以使用继承HandlerInterceptorAdapter
的方式, 这种方式可以按需覆盖父类方法.
创建 Servlet、 Filter、Listener
注解方式
直接通过
@WebServlet
、@WebFilter
、@WebListener
注解自动注册
1 | "customFilter", urlPatterns = "/*") (filterName = |
然后需要在**Application.java
加上@ServletComponentScan
注解, 否则不会生效.
注意: 如果同时添加了@WebFilter
以及@Component
, 那么会初始化两次Filter, 并且会过滤所有路径+自己指定的路径 , 便会出现对没有指定的URL也会进行过滤
通过编码注册
1 |
|
Spring Interceptor与Servlet Filter的区别
Filter
是基于函数回调的, 而Interceptor
则是基于Java反射的.Filter
依赖于Servlet容器, 而Interceptor
不依赖于Servlet容器.Filter
对几乎所有的请求起作用, 而Interceptor
只能对action
请求起作用.Interceptor
可以访问Action
的上下文, 值栈里的对象, 而Filter
不能.- 在
action
的生命周期里,Interceptor
可以被多次调用, 而Filter只能在容器初始化时调用一次.
RequestBodyAdvice和ResponseBodyAdvice
应用场景
- 对Request请求参数解密, 对Response返回参数进行加密
- 自定义返回信息(业务无关性的)
使用
先看一下ResponseBodyAdvice
1 | public interface ResponseBodyAdvice<T> { |
其中supports
方法指定是否需要执行beforeBodyWrite
, 其中参数returnType
可以拿到Controller对应方法中的方法注解以及参数注解: returnType.getMethodAnnotation(XXXAnnotation.class)
、returnType.getParameterAnnotation(XXXAnnotation.class)
.
beforeBodyWrite
可以对返回的body进行包装或加密:
1 | /** |
- 需要在类上面添加
@ControllerAdvice
或@RestControllerAdvice
才能生效
RequestBodyAdvice
的beforeBodyRead
在拦截器之后执行, 所以可以在拦截器做签名检验, 然后在RequestBodyAdvice
中解密请求参数
Spring Boot和Feign中使用Java 8时间日期API(LocalDate等)的序列化问题
http://blog.didispace.com/Spring-Boot-And-Feign-Use-localdate/
RequestBody 多读
有时候, 我们想要在过滤器或者拦截器中记录一下请求信息, POST 请求的 body 部分需要在 Request 中读取 InputStream. 但默认情况下只能读取一次, 可以通过继承 HttpServletRequestWrapper
实现:
1 | 4j |
Filter:
1 | public class RequestBodyCachingFilter extends OncePerRequestFilter { |
configuration:
1 | .class) (RequestBodyCachingCondition |
获取 requestBody:
1 | public static String getRequestBody() { |
Restful 性能优化
在 Spring MVC 中, 通过 @PathVariable
注解可轻松实现 Restful 风格的请求. 但是对于这种请求, Spring MVC 不能通过 url 直接获取到对应的 HandlerMethod
, 而是通过 for 循环一个个地匹配, 效率低下.
我们可以通过重写 RequestMappingHandlerMapping#lookupHandlerMethod
方法, 思路是, 如果是匹配类型的 restful 请求, 其真正映射到的是 @RequestMapping#name
, 请求时将 name
放在 Header 中, 查找的时候直接拿到通过 Hash 定位即可, 性能与直接匹配的效果一样.
继承 RequestMappingHandlerMapping
:
1 | public class EnhanceRequestMappingHandlerMapping extends RequestMappingHandlerMapping { |
配置:
1 | false) (proxyBeanMethods = |
跨域配置
Spring Mvc
方式一: 在 Controller 的类或者方法上贴上 @CrossOrigin
方式二: 上面的方式一需要每个类或者方法都加上, 有点麻烦, 可以使用 Spring 的 CorsFilter
:
1 |
|
Spring Security
1 |
|
OAuth2
集成了 OAth2 后, /oauth/token
会先发送一次 option 请求.
1 |
|
然后在 SecurityConfig 中开启跨域支持
1 |
|
Validation
常用注解(大部分JSR中已有)
注解 | 类型 | 说明 |
---|---|---|
@AssertFalse |
Boolean,boolean | 验证注解的元素值是false |
@AssertTrue |
Boolean,boolean | 验证注解的元素值是true |
@NotNull |
任意类型 | 验证注解的元素值不是null |
@Null |
任意类型 | 验证注解的元素值是null |
@Min(value=值) |
BigDecimal, BigInteger, byte,short, int, long, 等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值大于等于@Min指定的value值 |
@Max(value=值) |
和@Min要求一样 | 验证注解的元素值小于等于@Max指定的value值 |
@DecimalMin(value=值) |
和@Min要求一样 | 验证注解的元素值大于等于@ DecimalMin指定的value值 |
@DecimalMax(value=值) |
和@Min要求一样 | 验证注解的元素值小于等于@ DecimalMax指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) |
和@Min要求一样 | 验证注解的元素值的整数位数和小数位数上限 |
@Size(min=下限, max=上限) |
字符串、Collection、Map、数组等 | 验证注解的元素值的在min和max(包含)指定区间之内, 如字符长度、集合大小 |
@Past |
java.util.Date,java.util.Calendar;Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早 |
@Future |
与@Past要求一样 | 验证注解的元素值(日期类型)比当前时间晚 |
@NotBlank |
CharSequence子类型 | 验证注解的元素值不为空(不为null、去除首位空格后长度为0), 不同于@NotEmpty, @NotBlank只应用于字符串且在比较时会去除字符串的首位空格 |
@Length(min=下限, max=上限) |
CharSequence子类型 | 验证注解的元素值长度在min和max区间内 |
@NotEmpty |
CharSequence子类型、Collection、Map、数组 | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) |
BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 | 验证注解的元素值在最小值和最大值之间 |
@Email(regexp=正则表达式,flag=标志的模式) |
CharSequence子类型(如String) | 验证注解的元素值是Email, 也可以通过regexp和flag指定自定义的email格式 |
@Pattern(regexp=正则表达式,flag=标志的模式) |
String, 任何CharSequence的子类型 | 验证注解的元素值与指定的正则表达式匹配 |
@Valid |
任何非原子类型 | 指定递归验证关联的对象;如用户对象中有个地址对象属性, 如果想在验证用户对象时一起验证地址对象的话, 在地址对象上加@Valid注解即可级联验证 |
简单使用
实体:
1 |
|
Controller
:
1 |
|
快速失效
一般情况下, Validator并不会应为第一个校验失败为停止, 而是一直校验完所有参数. 我们可以通过设置快速失效:
1 |
|
这样在遇到第一个校验失败的时候就会停止对之后的参数校验.
分组校验
如果同一个类, 在不同的使用场景下有不同的校验规则, 那么可以使用分组校验. 未成年人是不能喝酒的, 而在其他场景下我们不做特殊的限制, 这个需求如何体现同一个实体, 不同的校验规则呢?
添加分组:
1 | Class Foo{ |
Controller
:
1 | "/drink") ( |
自定义校验
业务需求总是比框架提供的这些简单校验要复杂的多, 我们可以自定义校验来满足我们的需求. 自定义spring validation非常简单, 主要分为两步.
1 自定义校验注解
我们尝试添加一个“字符串不能包含空格”的限制.
1 | ({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) |
我们不需要关注太多东西, 使用spring validation的原则便是便捷我们的开发, 例如payload, List , groups, 都可以忽略.
<1>
自定义注解中指定了这个注解真正的验证者类.
2 编写真正的校验者类
1 | public class CannotHaveBlankValidator implements <1> ConstraintValidator<CannotHaveBlank, String> { |
<1>
所有的验证者都需要实现ConstraintValidator
接口, 它的接口也很形象, 包含一个初始化事件方法, 和一个判断是否合法的方法
1 | public interface ConstraintValidator<A extends Annotation, T> { |
<2>
ConstraintValidatorContext
这个上下文包含了认证中所有的信息, 我们可以利用这个上下文实现获取默认错误提示信息, 禁用错误提示信息, 改写错误提示信息等操作.
<3>
一些典型校验操作, 或许可以对你产生启示作用.
值得注意的一点是, 自定义注解可以用在METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER
之上, ConstraintValidator
的第二个泛型参数T, 是需要被校验的类型.
手动校验
可能在某些场景下需要我们手动校验, 即使用校验器对需要被校验的实体发起validate, 同步获得校验结果. 理论上我们既可以使用Hibernate Validation提供Validator, 也可以使用Spring对其的封装. 在spring构建的项目中, 提倡使用经过spring封装过后的方法, 这里两种方法都介绍下:
Hibernate Validation:
1 | Foo foo = new Foo(); |
由于依赖了Hibernate Validation框架, 我们需要调用Hibernate相关的工厂方法来获取validator实例, 从而校验.
在spring framework文档的Validation相关章节, 可以看到如下的描述:
Spring provides full support for the Bean Validation API. This includes convenient support for bootstrapping a JSR-303/JSR-349 Bean Validation provider as a Spring bean. This allows for a javax.validation.ValidatorFactory or javax.validation.Validator to be injected wherever validation is needed in your application. Use the LocalValidatorFactoryBean to configure a default Validator as a Spring bean:
bean id=”validator” class=”org.springframework.validation.beanvalidation.LocalValidatorFactoryBean”
The basic configuration above will trigger Bean Validation to initialize using its default bootstrap mechanism. A JSR-303/JSR-349 provider, such as Hibernate Validator, is expected to be present in the classpath and will be detected automatically.
上面这段话主要描述了spring对validation全面支持JSR-303、JSR-349的标准, 并且封装了LocalValidatorFactoryBean
作为validator的实现. 值得一提的是, 这个类的责任其实是非常重大的, 他兼容了spring的validation体系和hibernate的validation体系, 也可以被开发者直接调用, 代替上述的从工厂方法中获取的hibernate validator. 由于我们使用了springboot, 会触发web模块的自动配置, LocalValidatorFactoryBean
已经成为了Validator的默认实现, 使用时只需要自动注入即可.
1 |
|
<1>
真正使用过Validator
接口的读者会发现有两个接口, 一个是位于javax.validation
包下, 另一个位于org.springframework.validation
包下, 注意我们这里使用的是前者javax.validation
, 后者是spring自己内置的校验接口, LocalValidatorFactoryBean
同时实现了这两个接口.
<2>
此处校验接口最终的实现类便是LocalValidatorFactoryBean
.
基于方法校验
1 |
|
<1>
为类添加@Validated注解
<2> <3>
校验方法的返回值和入参
<4>
添加一个异常处理器, 可以获得没有通过校验的属性相关信息
基于方法的校验, 个人不推荐使用, 感觉和项目结合的不是很好.
统一处理验证异常
异常类型 | 描述 |
---|---|
ConstraintViolationException |
违反约束, javax扩展定义 |
BindException |
绑定失败, 如表单对象参数违反约束 |
MethodArgumentNotValidException |
参数无效, 如JSON请求参数违反约束 |
MissingServletRequestParameterException |
参数缺失 |
TypeMismatchException |
参数类型不匹配 |
1 |
|
相关代码: