此篇大部分是对Spring MVC的一个回顾以及JSR303中bean validation规范的学习
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 响应用户。
https://github.com/alibaba/fastjson/wiki/%E5%9C%A8-Spring-%E4%B8%AD%E9%9B%86%E6%88%90-Fastjson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.46</version>
</dependency>
两种方式:
WebMvcConfigurer @Configuration
public class WebMvcMessageConvertConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
SerializeConfig serializeConfig = SerializeConfig.globalInstance;
serializeConfig.put(BigInteger.class, ToStringSerializer.instance);
serializeConfig.put(Long.class, ToStringSerializer.instance);
serializeConfig.put(Long.TYPE, ToStringSerializer.instance);
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setCharset(Charset.forName("UTF-8"));
fastJsonConfig.setSerializeConfig(serializeConfig);
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(fastConverter);
}
}
@Bean 方式 @Configuration
public class WebMvcMessageConvertConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverter() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
SerializeConfig serializeConfig = SerializeConfig.globalInstance;
serializeConfig.put(BigInteger.class, ToStringSerializer.instance);
serializeConfig.put(Long.class, ToStringSerializer.instance);
serializeConfig.put(Long.TYPE, ToStringSerializer.instance);
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setCharset(Charset.forName(Constant.CHARSET));
fastJsonConfig.setSerializeConfig(serializeConfig);
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonConfig.setDateFormat(Constant.DATE_FORMAT);
fastConverter.setFastJsonConfig(fastJsonConfig);
return new HttpMessageConverters((HttpMessageConverter<?>) fastConverter);
}
}
上面针对的是Web MVC, 对于Webflux目前不支持这种方式 ,只能先这么设置
spring:
jackson:
default-property-inclusion: non_null # 过滤值为null的字段
date-format: "yyyy-MM-dd HH:mm:ss"
Spring boot 在spring默认基础上,自动配置添加了以下特性
ContentNegotiatingViewResolver 和 BeanNameViewResolver beans。 Converter , GenericConverter , Formatter beans。 HttpMessageConverters 的支持。 MessageCodeResolver 。 index.html 的支持。 Favicon 的支持。 ConfigurableWebBindingInitializer bean FreeMarker Thymeleaf Velocity (1.4版本之后弃用,Spring Framework 4.3版本之后弃用) Groovy Mustache 注: jsp应该尽量避免使用 ,原因如下:
server:
compression:
enabled: true # 启用压缩
min-response-size: 2048 # 对应Content-Length,超过这个值才会压缩
html 静态页面:在 resources/public/error/ 下定义. 如添加404页面: resources/public/error/404.html 页面,中文注意页面编码 templates/error/ 下定义. 如添加5xx页面: templates/error/5xx.ftl 注: templates/error/ 这个的优先级比较 resources/public/error/ 高
@Slf4j
@ControllerAdvice
//@RestControllerAdvice
public class ErrorExceptionHandler {
@ExceptionHandler({ RuntimeException.class })
@ResponseStatus(HttpStatus.OK)
public ModelAndView processException(RuntimeException exception) {
log.info("自定义异常处理-RuntimeException");
ModelAndView m = new ModelAndView();
m.addObject("roncooException", exception.getMessage());
m.setViewName("error/500");
return m;
}
@ExceptionHandler({ Exception.class })
@ResponseStatus(HttpStatus.OK)
public ModelAndView processException(Exception exception) {
log.info("自定义异常处理-Exception");
ModelAndView m = new ModelAndView();
m.addObject("roncooException", exception.getMessage());
m.setViewName("error/500");
return m;
}
}
或者继承 ResponseEntityExceptionHandler 更灵活地控制状态码、 Header 等信息:
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
// @ResponseStatus(HttpStatus.OK)
@ExceptionHandler(value = { Exception.class })
@Nullable
protected ResponseEntity<Object> handleConflict(Exception ex, WebRequest request) {
String bodyOfResponse = ex.getMessage();
HttpHeaders headers = new HttpHeaders();
headers.set(CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
return handleExceptionInternal(ex, bodyOfResponse, headers, HttpStatus.INTERNAL_SERVER_ERROR, request);
}
}
更多方式请看: http://www.baeldung.com/exception-handling-for-rest-with-spring
设置静态资源放到指定路径下
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/static/
@Bean
public StringHttpMessageConverter stringHttpMessageConverter() {
StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
return converter;
}
有些时候我们需要自己配置SpringMVC而不是采用默认,比如增加一个拦截器
public class MyInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
throws Exception {
System.out.println("拦截器MyInterceptor------->3、请求结束之后被调用,主要用于清理工作。");
}
@Override
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
throws Exception {
System.out.println("拦截器MyInterceptor------->2、请求之后调用,在视图渲染之前,也就是Controller方法调用之后");
}
@Override
public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception {
System.out.println("拦截器MyInterceptor------->1、请求之前调用,也就是Controller方法调用之前。");
return true;//返回true则继续向下执行,返回false则取消当前请求
}
}
@Configuration
public class InterceptorConfigurerAdapter extends WebMvcConfigurer {
/**
* 该方法用于注册拦截器
* 可注册多个拦截器,多个拦截器组成一个拦截器链
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns 添加路径
// excludePathPatterns 排除路径
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/*.*");
super.addInterceptors(registry);
}
}
直接通过 @WebServlet 、 @WebFilter 、 @WebListener 注解自动注册
@WebFilter(filterName = "customFilter", urlPatterns = "/*")
public class CustomFilter implements Filter {
...
}
@WebListener
public class CustomListener implements ServletContextListener {
...
}
@WebServlet(name = "customServlet", urlPatterns = "/roncoo")
public class CustomServlet extends HttpServlet {
...
}
然后需要在 **Application.java 加上 @ServletComponentScan 注解,否则不会生效。
注意:如果同时添加了 @WebFilter 以及 @Component ,那么会初始化两次Filter,并且会过滤所有路径+自己指定的路径 ,便会出现对没有指定的URL也会进行过滤
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
MyFilter filter = new MyFilter();
registrationBean.setFilter(filter);
List<String> urlPatterns = new ArrayList<>();
urlPatterns.add("/*");
registrationBean.setUrlPatterns(urlPatterns);
registrationBean.setOrder(1);
return registrationBean;
}
@Bean
public ServletRegistrationBean myServlet() {
MyServlet demoServlet = new MyServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean();
registrationBean.setServlet(demoServlet);
List<String> urlMappings = new ArrayList<String>();
urlMappings.add("/myServlet");////访问,可以添加多个
registrationBean.setUrlMappings(urlMappings);
registrationBean.setLoadOnStartup(1);
return registrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener() {
ServletListenerRegistrationBean registrationBean
= new ServletListenerRegistrationBean<>();
registrationBean.setListener(new MyListener());
registrationBean.setOrder(1);
return registrationBean;
}
}
Filter 是基于函数回调的,而 Interceptor 则是基于Java反射的。 Filter 依赖于Servlet容器,而 Interceptor 不依赖于Servlet容器。 Filter 对几乎所有的请求起作用,而 Interceptor 只能对 action 请求起作用。 Interceptor 可以访问 Action 的上下文,值栈里的对象,而 Filter 不能。 action 的生命周期里, Interceptor 可以被多次调用,而Filter只能在容器初始化时调用一次。
先看一下 ResponseBodyAdvice
public interface ResponseBodyAdvice<T> {
/**
* Whether this component supports the given controller method return type
* and the selected {@code HttpMessageConverter} type.
* @param returnType the return type
* @param converterType the selected converter type
* @return {@code true} if {@link #beforeBodyWrite} should be invoked;
* {@code false} otherwise
*/
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
/**
* Invoked after an {@code HttpMessageConverter} is selected and just before
* its write method is invoked.
* @param body the body to be written
* @param returnType the return type of the controller method
* @param selectedContentType the content type selected through content negotiation
* @param selectedConverterType the converter type selected to write to the response
* @param request the current request
* @param response the current response
* @return the body that was passed in or a modified (possibly new) instance
*/
T beforeBodyWrite(T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}
其中 supports 方法指定是否需要执行 beforeBodyWrite ,其中参数 returnType 可以拿到Controller对应方法中的方法注解以及参数注解: returnType.getMethodAnnotation(XXXAnnotation.class) 、 returnType.getParameterAnnotation(XXXAnnotation.class) 。
beforeBodyWrite 可以对返回的body进行包装或加密:
@RestControllerAdvice(annotations = Rest.class)
public class GlobalControllerAdvisor implements ResponseBodyAdvice {
private static final String VOID = "void";
private static final String RESOURCE_NOT_FOUND = "Resource not found!";
private static final String SUCCESS = "SUCCESS";
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return Boolean.TRUE;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
Object result;
if (isVoidMethod(returnType)) {
result = genSuccessResult(null, SUCCESS);
} else if (body instanceof Result) {
result = body;
} else if (nonNull(body)) {
result = genSuccessResult(body);
} else {
result = genBadReqResult(NOT_FOUND, RESOURCE_NOT_FOUND);
}
return result;
}
private boolean isVoidMethod(MethodParameter returnType) {
return VOID.equals(returnType.getMethod().getReturnType().getName());
}
}
@ControllerAdvice 或 @RestControllerAdvice 才能生效 RequestBodyAdvice 的 beforeBodyRead 在拦截器之后执行,所以可以在拦截器做签名检验,然后在 RequestBodyAdvice 中解密请求参数
| 注解 | 类型 | 说明 |
|---|---|---|
@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注解即可级联验证 |
实体:
@Data
public class Foo {
@NotBlank
private String name;
@Min(18)
private Integer age;
@Pattern(regexp = "^1([34578])//d{9}$",message = "手机号码格式错误")
@NotBlank(message = "手机号码不能为空")
private String phone;
@Email(message = "邮箱格式错误")
private String email;
}
Controller :
@RestController
@Slf4j
public class FooController {
@PostMapping("/foo")
public String foo(@Validated Foo foo, BindingResult bindingResult) {
log.info("foo: {}", foo);
if (bindingResult.hasErrors()) {
for (FieldError fieldError : bindingResult.getFieldErrors()) {
log.error("valid fail: field = {}, message = {}", fieldError.getField(), fieldError.getDefaultMessage());
}
return "fail";
}
return "success";
}
}
一般情况下,Validator并不会应为第一个校验失败为停止,而是一直校验完所有参数。我们可以通过设置快速失效:
@Configuration
public class ValidatorConfiguration {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
// .addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
这样在遇到第一个校验失败的时候就会停止对之后的参数校验。
如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。未成年人是不能喝酒的,而在其他场景下我们不做特殊的限制,这个需求如何体现同一个实体,不同的校验规则呢?
添加分组:
Class Foo{
@Min(value = 18,groups = {Adult.class})
private Integer age;
public interface Adult{}
public interface Minor{}
}
Controller :
@RequestMapping("/drink")
public String drink(@Validated({Foo.Adult.class}) Foo foo, BindingResult bindingResult) {
if(bindingResult.hasErrors()){
for (FieldError fieldError : bindingResult.getFieldErrors()) {
//...
}
return "fail";
}
return "success";
}
业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。自定义spring validation非常简单,主要分为两步。
1 自定义校验注解
我们尝试添加一个“字符串不能包含空格”的限制。
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {CannotHaveBlankValidator.class})<1>
public @interface CannotHaveBlank {
//默认错误消息
String message() default "不能包含空格";
//分组
Class<?>[] groups() default {};
//负载
Class<? extends Payload>[] payload() default {};
//指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
CannotHaveBlank[] value();
}
}
我们不需要关注太多东西,使用spring validation的原则便是便捷我们的开发,例如payload,List ,groups,都可以忽略。
<1> 自定义注解中指定了这个注解真正的验证者类。
2 编写真正的校验者类
public class CannotHaveBlankValidator implements <1> ConstraintValidator<CannotHaveBlank, String> {
@Override
public void initialize(CannotHaveBlank constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context <2>) {
//null时不进行校验
if (value != null && value.contains(" ")) {
<3>
//获取默认提示信息
String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
System.out.println("default message :" + defaultConstraintMessageTemplate);
//禁用默认提示信息
context.disableDefaultConstraintViolation();
//设置提示语
context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
return false;
}
return true;
}
}
<1> 所有的验证者都需要实现 ConstraintValidator 接口,它的接口也很形象,包含一个初始化事件方法,和一个判断是否合法的方法
public interface ConstraintValidator<A extends Annotation, T> {
void initialize(A constraintAnnotation);
boolean isValid(T value, ConstraintValidatorContext context);
}
<2> ConstraintValidatorContext 这个上下文包含了认证中所有的信息,我们可以利用这个上下文实现获取默认错误提示信息,禁用错误提示信息,改写错误提示信息等操作。
<3> 一些典型校验操作,或许可以对你产生启示作用。
值得注意的一点是,自定义注解可以用在 METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER 之上, ConstraintValidator 的第二个泛型参数T,是需要被校验的类型。
可能在某些场景下需要我们手动校验,即使用校验器对需要被校验的实体发起validate,同步获得校验结果。理论上我们既可以使用Hibernate Validation提供Validator,也可以使用Spring对其的封装。在spring构建的项目中,提倡使用经过spring封装过后的方法,这里两种方法都介绍下:
Foo foo = new Foo();
foo.setAge(22);
foo.setEmail("000");
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Validator validator = vf.getValidator();
Set<ConstraintViolation<Foo>> set = validator.validate(foo);
for (ConstraintViolation<Foo> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}
由于依赖了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的默认实现,使用时只需要自动注入即可。
@Autowired
Validator globalValidator; <1>
@RequestMapping("/validate")
public String validate() {
Foo foo = new Foo();
foo.setAge(22);
foo.setEmail("000");
Set<ConstraintViolation<Foo>> set = globalValidator.validate(foo);<2>
for (ConstraintViolation<Foo> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}
return "success";
}
<1> 真正使用过 Validator 接口的读者会发现有两个接口,一个是位于 javax.validation 包下,另一个位于 org.springframework.validation 包下, 注意我们这里使用的是前者 javax.validation ,后者是spring自己内置的校验接口, LocalValidatorFactoryBean 同时实现了这两个接口。
<2> 此处校验接口最终的实现类便是 LocalValidatorFactoryBean 。
@RestController
@Validated <1>
public class BarController {
@RequestMapping("/bar")
public @NotBlank <2> String bar(@Min(18) Integer age <3>) {
System.out.println("age : " + age);
return "";
}
@ExceptionHandler(ConstraintViolationException.class)
public Map handleConstraintViolationException(ConstraintViolationException cve){
Set<ConstraintViolation<?>> cves = cve.getConstraintViolations();<4>
for (ConstraintViolation<?> constraintViolation : cves) {
System.out.println(constraintViolation.getMessage());
}
Map map = new HashMap();
map.put("errorCode",500);
return map;
}
}
<1> 为类添加@Validated注解
<2> <3> 校验方法的返回值和入参
<4> 添加一个异常处理器,可以获得没有通过校验的属性相关信息
基于方法的校验,个人不推荐使用,感觉和项目结合的不是很好。
@ControllerAdvice
@Component
public class GlobalExceptionHandler {
@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handle(ValidationException exception) {
if(exception instanceof ConstraintViolationException){
ConstraintViolationException exs = (ConstraintViolationException) exception;
Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
/**打印验证不通过的信息*/
System.out.println(item.getMessage());
}
}
return "bad request, " ;
}
}
参考:
https://www.cnkirito.moe/2017/08/16/%E4%BD%BF%E7%94%A8spring%20validation%E5%AE%8C%E6%88%90%E6%95%B0%E6%8D%AE%E5%90%8E%E7%AB%AF%E6%A0%A1%E9%AA%8C/