Spring MVC 你必须关注点

时间:2022-07-22
本文章向大家介绍Spring MVC 你必须关注点,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Spring MVC配置简单,特别是在SpringBoot出现后基本都是开箱即用。在实际项目中通常是需要单独去处理一些特殊的情况,比如统一的异常处理,校验器以及国际化。

基础使用

为了简化相关的配置和包的引入,例子基于SpringBoot。首先引入相关的依赖包。

<parent>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-parent</artifactId>  <version>2.3.1.RELEASE</version></parent>
<dependencies>    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency></dependencies>

然后可以直接创建Controller类,即可实现一个基于SpringMVC的HTTP服务。

@RestControllerpublic class ExampleController {
    @RequestMapping("/")    String home() {        return "Hello World!";    }}

@SpringBootApplicationpublic class Application {
    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }
}

@RequestMapping 表示当前方法所对应的 Contextpath

@RestController 表示当前类,为Controller类,并且每个方法的ViewResolver都是采用JSON的方式来渲染。

异常处理

Controller发生了异常该如何处理。直接抛出异常,这是一种不可取得行为,对前端不友好,而且也可能暴露服务端的一些细节,给网络攻击提供一些便利的信息。每个Controller的方法都使用try ... catch 包裹住,这样的话代码的冗余度非常的高。这很容易让人想到了面向切面编程,SpringMVC提供了一个ControllerAdvice 机制来处理这种情况。这个注解的意义是拦截所有你在里面定义的异常。

@ControllerAdvicepublic class CustomErrorController {
  private static final Logger LOGGER = LoggerFactory.getLogger(CustomErrorController.class);
  @ExceptionHandler({ RuntimeException.class })  @ResponseStatus(HttpStatus.OK)  public @ResponseBody HttpResponse<Void> processException(RuntimeException ex) {    LOGGER.error(this.getClass().getName(), ex);    HttpResponse<Void> response = new HttpResponse<>();    response.fail().setMsg(ex.getMessage());    return response;  }
  @ExceptionHandler({ Exception.class })  @ResponseStatus(HttpStatus.OK)  public @ResponseBody HttpResponse<Void> processException(Exception ex) {    LOGGER.error(this.getClass().getName(), ex);    HttpResponse<Void> response = new HttpResponse<>();    response.fail().setMsg(ex.getMessage());    return response;  }
  @ExceptionHandler({ BindException.class })  @ResponseStatus(HttpStatus.OK)  public @ResponseBody HttpResponse<Void> processException(BindException ex) {
    LOGGER.error(this.getClass().getName(), ex.getMessage());
    HttpResponse<Void> response = new HttpResponse<>();    response.fail();
    response.setMsg(getErrorMessage(ex));    return response;  }}

如上述代码 ExceptionHandler 注解在某个方法上表示的是该方法处理该注解所标识的异常。这里面是统一对异常进行处理返回了自定义的HttpResponse对象。

通过ControllerAdvice能解决请求到达了Controller后的所有的异常,但是如果还未到达业务逻辑所产生的异常同样是会直接抛到前端去,正好SpringMVC框架在处理路由的时候如果没有找到路由是会产生这样的异常。

/** * 未处理错误页面  *  * 由于Spring MVC 的 DispatchServlet.throwExceptionIfNoHandler 直接返回了 404错误 *    * 404错误还没到Controller,无法被 ControllerAdvice捕获  * 需要单独的错误处理 */@RestControllerpublic class NotFoundController implements ErrorController{
  private static final String PATH = "/error";    public String getErrorPath() {    return PATH;  }    @RequestMapping(PATH)  public HttpResponse<Void> handler(HttpServletRequest request){    HttpResponse<Void> response = new HttpResponse<>();        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");    if(Objects.equal(statusCode, org.apache.http.HttpStatus.SC_NOT_FOUND)){      response.setCode(statusCode);    }    else{      response.fail().setMsg("unknown error");    }        return response;  }
}

上述代码定义了一个通用error处理的页面,当框架抛出异常,会转到 /error地址,我们对其进行了定制。

数据校验

web环境的输入比较复杂,后端需要对输入做好保底的业务正确性校验。Spring MVC 提供了两种方法来对用户的输入数据进行校验,一种是 Spring 自带的 Validation 校验框架,另一种是利用 JRS-303 验证框架进行验证。通常使用 JRS-303 ,代表性的框架为 Hibernate-Validator,它所包含的功能如下表。

注解

功能

@Null

验证对象是否为null

@NotNull

验证对象是否不为null

@AssertTrue

验证Boolean对象是否为true

@AssertTrue

验证Boolean对象是否为false

@Max(value)

验证Number和String对象是否小于等于指定值

@Min(value)

验证Number和String对象是否大于等于指定值

@DecimalMax(value)

验证注解的元素值小于等于@DecimalMax指定的value值

@DecimalMin(value)

验证注解的元素值大于等于@DecimalMin指定的value值

@Digits(integer,fraction)

验证字符串是否符合指定格式的数字,integer指定整数精度,fraction指定小数精度

@Size(min,max)

验证对象长度是否在给定的范围内

@Past

验证Date和Calendar对象是否在当前时间之前

@Future

验证Date和Calendar对象是否在当前时间之后

@Pattern

验证String对象是否符合正则表达式的规则

@NotBlank

检查字符串是不是Null,被Trim的长度是否大于0,只对字符串,且会去掉前后空格

@URL

验证是否是合法的url

@Email

验证是否是合法的邮箱

@CreditCardNumber

验证是否是合法的信用卡号

@Length(min,max)

验证字符串的长度必须在指定范围内

@NotEmpty

检查元素是否为Null或Empty

使用这些注解来标注接收参数的表单对象,然后在需要校验的时候使用@Validated注解进行标注。

@Datapublic class User {
    @NotBlank(message = "用户名不能为空")    private String username;      @NotBlank(message = "密码不能为空")    @Length(min = 6, max = 16, message = "密码的长度必须在6~16位之间")  private String password;      @Range(min = 18, max = 60, message = "年龄必须在18岁到60岁之间")    private Integer age;      @Pattern(regexp = "^1[3|4|5|7|8][0-9]{9}$", message = "请输入正确格式的手机号")    private String phone;      @Email(message = "请输入合法的邮箱地址")    private String email;}
@RestControllerpublic class UserController {
  @RequestMapping("/register")    public HttpResponse register(@Validated User user) {        // logic    }}

当然以上注解在实际项目中远远不够用,有一些业务的校验本身就比较复杂。在参数解析的时候进行校验的话,还需要做很多跟业务相关的逻辑,但是如果把校验逻辑放到Controller或者Service里面又显得很服务非常复杂,并且校验逻辑无法复用。SpringMVC支持我们进行校验器的自定义。

@Documented@Constraint(validatedBy = UserValidaror.class)@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface UserConstraint {
  String message() default "";    Class<?>[] groups() default {};    Class<? extends Payload>[] payload() default {};  }
@Conpementpublic class UserValidaror implements ConstraintValidator<UserConstraint, User>  {    public void initialize(UserConstraint constraintAnnotation) {      }
  public boolean isValid(User value, ConstraintValidatorContext context) {    // 比如校验邮箱或者电话的唯一性,或者其他需要通过服务调用    }}
@Data@UserConstraintpublic class User {
    @NotBlank(message = "用户名不能为空")    private String username;      @NotBlank(message = "密码不能为空")    @Length(min = 6, max = 16, message = "密码的长度必须在6~16位之间")  private String password;      @Range(min = 18, max = 60, message = "年龄必须在18岁到60岁之间")    private Integer age;      @Pattern(regexp = "^1[3|4|5|7|8][0-9]{9}$", message = "请输入正确格式的手机号")    private String phone;      @Email(message = "请输入合法的邮箱地址")    private String email;}

如上述代码,我们需要定义一个UserConstraint 注解,它将使用在表单接收模型User类上面,同时UserConstraint定义的时候即指定其对应的校验器类UserValidaror。UserValidaror实现了ConstraintValidator接口,使用isValid方法进行校验逻辑的业务实现。在使用的时候UserValidaror需要托管到Spring进行实例化。

分组校验

Constraint 注解都有一个group属性,用来指定校验的分组。因为并不是每一个操作需要校验所有的属性,比如新增和更新 校验的参数不一样。那么我们就可以定义两个分组。

@UserConstraint(groups={Create.class,Update.class})public class User {
    @NotBlank(message = "用户名不能为空",groups={Create.class})    private String username;        ...}
@RestControllerpublic class UserController {
  @RequestMapping("/register")    public HttpResponse register(@Validated(Create.class) User user) {        // logic    }        @RequestMapping("/update")    public HttpResponse register(@Validated(Update.class) User user) {        // logic    }}

在使用校验注解的时候指定了该注解的生效分组,如果没有指定的话则全部分组生效。再使用@Validated 指定校验的分组,则可以实现不同类型的操作,校验不同的内容。

国际化

在校验环节,我们直接把message放到了代码中。除了调整不方便,每次都需要重新编译和发布版本。还不能支持多语言。Spring Core 本身就有一个MessageSource 接口,用来实现各种消息的翻译。

@Configurationpublic class WebMvcConfs extends WebMvcConfigurerAdapter {      @Bean  public MessageSource messageSource(){    ResourceBundleMessageSource  messageSource = new ResourceBundleMessageSource ();        messageSource.setBasename("i18n/message");    messageSource.setCacheSeconds(300);    messageSource.setDefaultEncoding("UTF-8");        return messageSource;  }    // Validator 注入i18n 信息  public Validator getValidator() {    LocalValidatorFactoryBean factory = new LocalValidatorFactoryBean();        factory.setValidationMessageSource(messageSource());        return factory;  }}

上述代码配置了SpringMVC的 MessageSource实现和对Validator注入 翻译的MessageSource。它会根据Http Header中的Locale 来决定取哪个文件的配置来解析消息。比如locale 是zh_CN那么会取classpath下的i18n/message_zh_CN.properties来查找消息的对应翻译,如果查找不到则使用i18n/message.properties,兜底的文件没有的话则会发生异常,走入异常逻辑处理的环节。

那么要实现一个多语言的网站就比较简单了,只需要在界面上设置一个选择语言的交互界面。选择后设置对应的Locale,后续的请求和返回的内容则可以根据Locale来定制。

Validator 在引入了国际化的内容后,配置会有一些差别。首先我们不需要在配置注解里面写message,而是配置到对应的MessageSource文件里。

public class User { @NotBlank private String username; @Range(min = 18, max = 60) private Integer age;} // i18n/message.propertiesNotBlank.user.username=username can not be blankRange.user.age=age must between {min} in {max} // i18n/message_zh_CN.propertiesNotBlank.user.username=用户名不能为空Range.user.age=年龄必须在{min}岁到{max}岁之间

在定义i18n文件的时候可以使用变量,比如上述的Range注解对应Validate把min和max作为变量传入到校验后的结果中。那么配合国际化的时候我们的自定义注解也是可以做到。

@Conpementpublic class UserValidaror implements ConstraintValidator<UserConstraint, User>  {    public void initialize(UserConstraint constraintAnnotation) {      }
  public boolean isValid(User value, ConstraintValidatorContext context) {    // 比如校验邮箱或者电话的唯一性,或者其他需要通过服务调用        if (/* some condition */) {            HibernateConstraintValidatorContext hibernateValidatorContext = constraintValidatorContext.unwrap(HibernateConstraintValidatorContext.class);            hibernateValidatorContext.disableDefaultConstraintViolation();            hibernateValidatorContext.addMessageParameter("age", "some value...").buildConstraintViolationWithTemplate("{Range.user.age}")                .addPropertyNode("age").addConstraintViolation();            return false;        }    }}

整合使用

除了校验的异常需要进行国际化,服务端使用返回码来提示的业务错误也需要进行国际化消息提醒。那么异常处理可以定义一个ServiceException 统一包装来处理。那么ControllerAdvice 可以增加以下两个处理方法

@ExceptionHandler({ BindException.class })@ResponseStatus(HttpStatus.OK)public @ResponseBody HttpResponse<Void> processException(BindException ex) {  HttpResponse<Void> response = new HttpResponse<>();    response.fail();    response.setMsg(getErrorMessage(ex));  return response;}
@ExceptionHandler({ ServiceException.class })@ResponseStatus(HttpStatus.OK)public @ResponseBody HttpResponse<Void> processException(ServiceException ex) {  HttpResponse<Void> response = new HttpResponse<>();    response.fail();    response.setMsg(getErrorMsg(ex));  return response;}
/** * 获取参数错误信息 * @param ex * @return */private String getErrorMsg(BindException ex){  String message = null;    List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();    if(CollectionUtils.isEmpty(fieldErrors)){    return messageSource.getMessage("Param.Error", new Object[]{},"参数错误",RequestContextUtils.getLocale(request));  }    FieldError fieldError = fieldErrors.get(0);  String[] codes = fieldError.getCodes();    for(String code:codes){    message = messageSource.getMessage(code, fieldError.getArguments(), RequestContextUtils.getLocale(request));    //最明细的消息有的话就直接返回    if(StringUtils.isNotEmpty(message)){      break;    }  }    //如果没有定义i18n 信息 ,则取默认的  if(StringUtils.isEmpty(message)){    message = fieldError.getField() + fieldError.getDefaultMessage();  }    return message;}
private String getErrorMsg(ServiceException ex){  String message = message = messageSource.getMessage(ex.getCode(), ex.getArguments(), RequestContextUtils.getLocale(request));    if(StringUtils.isEmpty(message)){    messageSource.getMessage(/**unknow exception code*/, new Obejct[]{}, RequestContextUtils.getLocale(request));  }    return message;}

BindException 为 校验器默认的异常,ServiceException为自定义异常。分别对他们的以及内容进行i18n信息的翻译。