前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Spring MVC 你必须关注点

Spring MVC 你必须关注点

作者头像
李鸿坤
发布于 2020-07-23 11:27:26
发布于 2020-07-23 11:27:26
75000
代码可运行
举报
文章被收录于专栏:泛泛聊后端泛泛聊后端
运行总次数:0
代码可运行

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

基础使用

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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服务。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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 机制来处理这种情况。这个注解的意义是拦截所有你在里面定义的异常。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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框架在处理路由的时候如果没有找到路由是会产生这样的异常。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/** * 未处理错误页面  *  * 由于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注解进行标注。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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支持我们进行校验器的自定义。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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属性,用来指定校验的分组。因为并不是每一个操作需要校验所有的属性,比如新增和更新 校验的参数不一样。那么我们就可以定义两个分组。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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 接口,用来实现各种消息的翻译。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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作为变量传入到校验后的结果中。那么配合国际化的时候我们的自定义注解也是可以做到。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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 可以增加以下两个处理方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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信息的翻译。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 泛泛聊后端 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
从进程组、会话、终端的概念深入理解守护进程
「守护进程」是 Linux 的一种长期运行的后台服务进程,也有人称它为「精灵进程」。我们常见的 httpd、named、sshd 等服务都是以守护进程 Daemon 方式运行的,通常服务名称以字母d结尾,也就是 Daemon 第一个字母。与普通进程相比它大概有如下特点:
用户3105362
2021/02/04
1.2K0
进程组、会话、控制终端概念,如何创建守护进程?
守护进程,也就是通常所说的Daemon进程,是Linux中的后台服务进程。周期性的执行某种任务或等待处理某些发生的事件。
睡魔的谎言
2020/11/25
1.6K0
linux系统编程之进程(五):终端、作业控制与守护进程
该文介绍了如何在Linux系统中通过fork函数创建守护进程,并给出了具体的示例代码。同时,文章还介绍了守护进程的一些常见用途,如保证程序在后台运行、处理控制台输入输出等。
s1mba
2018/01/03
2.8K0
linux系统编程之进程(五):终端、作业控制与守护进程
Linux 守护进程|应急响应
通常我们都是通过以上两种方式来获得一个shell,之后运行程序的,此时我需要纠正一个概念,我们通常都说获得一个shell,本质上来说,我们获取了一个session(会话,以下session都是会话)
意大利的猫
2021/03/18
4.1K0
Linux 守护进程|应急响应
Linux内核编程--进程组和守护进程
进程组:进程组是多个进程的集合, 接收同一个终端的各类信号信息。进程调用setpgid(pid, pgid)可以加入一个现有的进程组或者创建一个新的进程组。
Coder-ZZ
2022/05/09
3.2K0
Linux内核编程--进程组和守护进程
【在Linux世界中追寻伟大的One Piece】进程间关系与守护进程
其实每一个进程除了有一个进程ID(PID)之外,还属于一个进程组。进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。 每一个进程组也有一个唯一的进程组ID(PGID), 并且这个 PGID类似于进程ID, 同样是一个正整数, 可以存放在pid_t数据类型中。
枫叶丹
2024/09/24
1330
【在Linux世界中追寻伟大的One Piece】进程间关系与守护进程
什么是守护进程?
在了解守护进程之前,需要先知道什么是什么是终端?什么是作业?什么是进程组?什么是会话?
全栈程序员站长
2022/09/07
1.1K0
【Linux网络编程】:守护进程,前台进程,后台进程
●无控制终端:脱离控制终端,避免收到终端的干扰,它是和客户端进行交流的。和Xshell终端摆脱了联系。
用户11396661
2025/02/04
3480
【Linux网络编程】:守护进程,前台进程,后台进程
linux 后台运行进程:& , nohup
当我们在终端或控制台工作时,可能不希望由于运行一个作业而占住了屏幕,因为可能还有更重要的事情要做,比如阅读电子邮件。对于密集访问磁盘的进程,我们更希望它能够在每天的非负荷高峰时间段运行(例如凌晨)。为了使这些进程能够在后台运行,也就是说不在终端屏幕上运行,有几种选择方法可供使用。
DevOps在路上
2023/05/16
5.3K0
linux 后台运行进程:& , nohup
将 Web 应用丢给守护进程
最近老是要把 Web App/Service 部署在个人的服务器上进行测试,发现自己不怎么熟悉「前提:不上 docker ,逃~」,特写此文章来纪念下??(之前部署的 Web App/Service
Cloud-Cloudys
2020/07/07
1.6K0
将 Web 应用丢给守护进程
Linux - 请允许我静静地后台运行
枕边书
2018/01/04
1.8K0
Linux - 请允许我静静地后台运行
【计算机网络】日志与守护进程
一般使用cout进行打印,但是cout打印是不规范的 实际上 是采用日志进行打印的
lovevivi
2023/11/17
2210
【计算机网络】日志与守护进程
守护进程
在Linux中,session(会话)通常指的是与用户交互的一个环境,它是系统中与某个用户交互的一系列活动的集合。会话在Linux系统中有多种用途,下面是几种常见的会话类型及其相关概念:
ljw695
2025/01/03
2780
守护进程
守护进程「建议收藏」
在UNIX系统中, 用户通过终端登录系统后得到一个Shell进程, 这个终端成为Shell进程的控制终端(Controlling Terminal), 进程中, 控制终端是保存在PCB中的信息, 而fork会复制PCB中的信息, 因此由Shell进程启动的其它进程的控制终端也是这个终端. 默认情况下(没有重定向), 每个进程的标准输入, 标准输出和标准错误输出都指向控制终端, 进程从标准输入读也就是读用户的键盘输入, 进程往标准输出或标准错误输出写也就是输出到显示器上. 信号中还讲过, 在控制终端输入一些特殊的控制键可以给前台进程发信号, 例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT。
全栈程序员站长
2022/09/16
6340
Python实现守护进程
專 欄 ❈汤英康,Python程序员,负责设计和开发大数据监控平台的相关产品。 PyCon China2016 深圳 讲师。 博客:http://blog.tangyingkang.com/ ❈— Daemon场景 考虑如下场景:你编写了一个python服务程序,并且在命令行下启动,而你的命令行会话又被终端所控制,python服务成了终端程序的一个子进程。因此如果你关闭了终端,这个命令行程序也会随之关闭。 要使你的python服务不受终端影响而常驻系统,就需要将它变成守护进程。 守护
Python中文社区
2018/01/31
2K0
Python守护进程daemon实现
守护进程是系统中生存期较长的一种进程,常常在系统引导装入时启动,在系统关闭时终止,没有控制终端,在后台运行。守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断。 在这里,我们在Linux2.6内核的centos中,ps -ef |awk '{print $1"\t "$2"\t "$3"\t  "$8}'看到:PPID=0的进程有两个,分别是PID=1的/sbin/init进程和PID=2的[kthreadd]进程。
py3study
2020/01/07
7.9K0
PHP中的会话
2、当执行php xxx.php 时,默认系统会把当前的进程设置为会话首进程(使用strace查看),所以当前会话首进程不能使用posix_setsid 创建为会话首进程,只能使用子进程调用此函数
北溟有鱼QAQ
2021/06/08
1.4K0
nohup、&、setsid、fork和fg、bg究竟有啥区别?
在后台运行的进程不一定是守护进程!一个进程要成为守护进程,必须做到以下两点:
一见
2018/08/10
2.4K0
Linux守护进程
守护进程在 Linux 系统中极为重要,它们是许多服务器的核心组成部分,例如 Internet 服务器 inetd 和 Web 服务器 httpd。这些进程不仅负责提供网络服务,还执行各种系统任务,例如作业调度进程 crond。
不脱发的程序猿
2024/11/26
3920
Linux守护进程
Linux守护进程
进程组,也叫做作业。BSD于1980年前后向Unix中增加的一个新特性,代表一个或者多个进程的集合,每个进程都属于一个进程组。操作系统设计进程组的概念主要就是为了简化对多个进程的管理。
mindtechnist
2024/08/08
3670
Linux守护进程
相关推荐
从进程组、会话、终端的概念深入理解守护进程
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档