1. 拦截器

拦截器是属于SpringMVC中的知识,顾名思义,就是针对请求做拦截用的

SpringMVC中,请求最终映射的是Controller方法,也就是说,拦截器是针对controller层的方法做拦截的,在方法前进行拦截,但是可以在方法后和视图渲染后执行其他操作

应用场景:

  1. 权限校验
  2. 登录拦截
  3. 日志记录
  4. 业务处理(比如12306购票前判断时间是否可以购票)

1.1 使用

SpringMVC 中的Interceptor 拦截请求是通过HandlerInterceptor 来实现的

  1. 实现HandlerInterceptor接口

    package com.mszlu.handler;
    
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    
    public class MyInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println("pre handler....");
            //true放行  false拦截
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println("post handler....");
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println("after completion...");
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
  2. 配置拦截器,使其生效

    spring-mvc.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="com.mszlu.controller" />
        <!--开启mvc的注解支持-->
        <mvc:annotation-driven/>
    
        <!--扫包的配置,在spring配置文件中 已经定义,保证controller在扫包范围内即可-->
    
        <!--过滤静态文件-->
        <mvc:default-servlet-handler />
        
        <mvc:interceptors>
            <!--可以配置多个拦截器-->
            <mvc:interceptor>
                <!--拦截路径-->
                <mvc:mapping path="/user/**"/>
                <!--拦截器-->
                <bean class="com.mszlu.handler.MyInterceptor"/>
            </mvc:interceptor>
        </mvc:interceptors>
    </beans>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
  3. 测试, 启动访问http://localhost/user/getUser/1

1.2 参数意义

	@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("pre handler....");
        //handler: 在springmvc的执行流程中,涉及到HandlerAdapter根据路径映射找到对应的Handler,Handler是controller的一个方法,在这的handler指的就是controller的方法,当然由于一个请求不一定是访问controller的方法,比如读取配置文件,比如404,所以,handler分为两种:
       // 1. HandlerMethod 代表controller的方法
       // 2. HttpRequestHandler 请求处理器 
        //true放行,继续执行下一步  false拦截,不在执行接下来的流程
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("post handler....");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("after completion...");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

1.3 多拦截器

从配置上来看,拦截器是可以配置多个的,我们做三个拦截器看一下执行顺序

<mvc:interceptors>
        <!--可以配置多个拦截器-->
        <!--拦截路径-->

        <mvc:interceptor>
            <mvc:mapping path="/user/**"/>
            <!--拦截器-->
            <bean class="com.mszlu.handler.MyInterceptor1"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/user/**"/>
            <!--拦截器-->
            <bean class="com.mszlu.handler.MyInterceptor2"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/user/**"/>
            <!--拦截器-->
            <bean class="com.mszlu.handler.MyInterceptor3"/>
        </mvc:interceptor>
    </mvc:interceptors>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.mszlu.handler;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class MyInterceptor1 implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("pre1");
        //true放行  false拦截
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("post1");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("after1");
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.mszlu.handler;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class MyInterceptor2 implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("pre2");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("post2");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("after2");
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.mszlu.handler;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class MyInterceptor3 implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("pre3");
        //true放行  false拦截
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("post3");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("after3");
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

结论:

  1. 如果preHandle都返回true

    pre1
    pre2
    pre3
    getUser方法调用...
    post3
    post2
    post1
    after3
    after2
    after1
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  2. 如果MyInterceptor1的preHandle返回false

    pre1
    
    1
  3. 如果MyInterceptor2的preHandle返回false

    pre1
    pre2
    after1
    
    1
    2
    3
  4. 如果MyInterceptor3的preHandle返回false

    pre1
    pre2
    pre3
    after2
    after1
    
    1
    2
    3
    4
    5

通过实验,有一个比较有意思的结论,只要pre放行,那么after就一定执行,只要任意一个pre返回false,所有的post都不执行

2. 统一异常处理

统一异常要用到@ControllerAdvice和@ExceptionHandler这两个注解,从名字上可以得出,@ControllerAdvice是对加了@Controller注解的类进行切面处理

为什么要有统一异常呢?

  1. 如果是编码过程中没有预料到的异常,比如bug,内存不足,硬件错误等等造成的,会提示给用户一些不友好的信息,如果在controller中处理,那么每个controller都要try catch,又过于繁琐
  2. 异常的种类很多,有自定义的业务异常,有系统异常,有空指针异常,有数组越界等,每种异常处理的方式都不一致,比如业务异常,需要提示用户准确信息,系统异常需要提示用户友好信息(系统繁忙,请稍候再试)并且记录错误,空指针异常遇到发送错误日志短信给开发人员等

2.1 使用

package com.mszlu.handler;

import lombok.Data;
//统一结果返回
@Data
public class Result {

    private boolean success;

    private int code;

    private String message;

    private Object data;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.mszlu.handler;

public class BusinessException extends RuntimeException {

    private int code;

    public BusinessException(){
        super();
    }

    public BusinessException(Integer code,String message){
        super(message);
        this.code = code;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.mszlu.handler;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result doException(Exception e){
        //记录异常
        e.printStackTrace();
        Result result = new Result();
        result.setCode(-999);
        result.setMessage("未知的异常,提示友好信息");
        result.setSuccess(false);
        return result;
    }

    @ExceptionHandler(IndexOutOfBoundsException.class)
    @ResponseBody
    public Result doStack(IndexOutOfBoundsException e){
        //记录异常
        e.printStackTrace();
        //发送短信给开发人员
    //        send()
        Result result = new Result();
        result.setCode(-999);
        result.setMessage("数组越界异常");
        result.setSuccess(false);
        return result;
    }

    @ExceptionHandler(BusinessException.class)
    @ResponseBody
    public Result doBusiness(BusinessException e){
        Result result = new Result();
        result.setCode(e.getCode());
        result.setMessage(e.getMessage());
        result.setSuccess(false);
        return result;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

测试:

@GetMapping("getUser/{id}")
    public User findUser(@PathVariable Long id){
        System.out.println("getUser方法调用...");
        if (id == 2){
            throw new BusinessException(-999,"对不起,参数不能为2");
        }
        if (id == 3){
            throw new IndexOutOfBoundsException();
        }
        if (id == 4){
            int i = 10/0;
        }
        return userService.getUser(id);
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

注意ERROR类型的错误 捕获不到

3. 数据校验

在进行请求的时候,大多数往往会携带参数,我们总需要对参数进行合法性的校验,比如不能为null,密码长度不能小于8,用户名必须使用大小写字母+数字等等

JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,通过 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证。

image-20220216112917672

Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解

image-20220216112938998

3.1 使用

需要导入依赖:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.0.Final</version>
</dependency>
1
2
3
4
5

tomcat7不支持,此版本,所以使用tomcat9插件:

<build>
        <!--设置插件-->
        <plugins>
            <!--tomcat9的插件 插件配置-->
            <plugin>
                <groupId>org.opoo.maven</groupId>
                <artifactId>tomcat9-maven-plugin</artifactId>
                <version>3.0.0</version>
                <configuration>
                    <port>80</port>
                    <path>/</path>
                </configuration>
            </plugin>
        </plugins>
    </build>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

需求:对用户名进行校验,不能为null,对年龄进行校验,不能小于18岁,不能大于50岁

 @PostMapping("valid")
//在参数前面 加@Valid注解
    public Result valid(@Valid @RequestBody User user) {
        return new Result(true,200,"success",user);
    }
1
2
3
4
5
package com.mszlu.pojo;

import lombok.Data;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Data
public class User {

    private Long id;

    @NotNull(message = "用户名不能为空")
    private String name;
    @Min(value = 18,message = "年龄不能小于18")
    @Max(value = 50,message = "年龄不能大于50")
    private Integer age;

    private String email;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

使用postman进行测试:

{
    "id":1,
    "name":"zhangsan",
    "age":15
}
1
2
3
4
5

测试发现,错误会打印在控制台。

如何在方法上,获取参数校验异常呢?

@PostMapping("valid")
    //添加Errors,spring提供的,可以获取到参数校验的错误信息
    public Result valid(@Valid @RequestBody User user, Errors errors) {
        if (errors.hasErrors()){
            //获取校验有错误的字段
            List<FieldError> fieldErrors = errors.getFieldErrors();
            for (FieldError fieldError : fieldErrors) {
                System.out.println(fieldError.getField());
                System.out.println(fieldError.getDefaultMessage());
                System.out.println("------------------------------");
            }
        }
        return new Result(true,200,"success",user);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

控制台信息:

age
年龄不能小于18
------------------------------
1
2
3

3.2 复杂类型校验

如果User中有一个Address对象,需要对Address中的city字段进行校验呢?

package com.mszlu.pojo;

import lombok.Data;

import javax.validation.constraints.NotNull;

@Data
public class Address {

    @NotNull(message = "城市不能为空")
    private String city;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.mszlu.pojo;

import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Data
public class User {

    private Long id;

    @NotNull(message = "用户名不能为空")
    private String name;
    @Min(value = 18,message = "年龄不能小于18")
    @Max(value = 50,message = "年龄不能大于50")
    private Integer age;

    private String email;
	//注意要在这里加@Valid注解
    @Valid
    @NotNull(message = "地址不能为空")
    private Address address;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
    "id":1,
    "name":"zhangsan",
    "age":19,
    "address":{}
}
1
2
3
4
5
6

3.3 分组校验

实际工作中,会出现以下情况,一个接口需要验证name不为空,一个接口不需要验证,这就需要使用分组校验,使用分组校验需要使用@Validated注解,是spring提供的

@PostMapping("valid")
//添加Errors,spring提供的,可以获取到参数校验的错误信息
//其中GroupA是接口
    public Result valid(@Validated(value = GroupA.class) @RequestBody User user, Errors errors) {
        if (errors.hasErrors()){
            //获取校验有错误的字段
            List<FieldError> fieldErrors = errors.getFieldErrors();
            for (FieldError fieldError : fieldErrors) {
                System.out.println(fieldError.getField());
                System.out.println(fieldError.getDefaultMessage());
                System.out.println("------------------------------");
            }
        }
        return new Result(true,200,"success",user);
    }

    @PostMapping("valid1")
    //添加Errors,spring提供的,可以获取到参数校验的错误信息
    public Result valid1(@Validated @RequestBody User user, Errors errors) {
        if (errors.hasErrors()){
            //获取校验有错误的字段
            List<FieldError> fieldErrors = errors.getFieldErrors();
            for (FieldError fieldError : fieldErrors) {
                System.out.println(fieldError.getField());
                System.out.println(fieldError.getDefaultMessage());
                System.out.println("------------------------------");
            }
        }
        return new Result(true,200,"success",user);
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.mszlu.pojo;

import com.mszlu.group.GroupA;
import com.mszlu.group.GroupB;
import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@Data
public class User {

    private Long id;

    @NotNull(message = "用户名不能为空")
    private String name;
    @Min(value = 18,message = "年龄不能小于18",groups = GroupA.class)
    @Max(value = 50,message = "年龄不能大于50",groups = GroupA.class)
    private Integer age;

    private String email;

    @Valid
    @NotNull(message = "地址不能为空",groups = GroupA.class)
    private Address address;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

上述的代码表示:valid1接口 需要验证name不为空,valid接口不需要验证

参数:

{
    "id":1,
    "age":19,
    "address":{}
}
1
2
3
4
5

4. 国际化

在开发应用程序的时候,经常会遇到支持多语言的需求,这种支持多语言的功能称之为国际化,英文是internationalization,缩写为i18n(因为首字母i和末字母n中间有18个字母)。

还有针对特定地区的本地化功能,英文是localization,缩写为L10n,本地化是指根据地区调整类似姓名、日期的显示等。

也有把上面两者合称为全球化,英文是globalization,缩写为g11n。

如何实现国际化呢?需要先判断用户的语言,然后将不同语言的资源提取出来,渲染页面

4.1 获取Locale

实现国际化的第一步是获取到用户的Locale。在Web应用程序中,HTTP规范规定了浏览器会在请求中携带Accept-Language头,用来指示用户浏览器设定的语言顺序,如:

Accept-Language: zh-CN,zh;q=0.8,en;q=0.2
1

上述HTTP请求头表示优先选择简体中文,其次选择中文,最后选择英文。q表示权重,解析后我们可获得一个根据优先级排序的语言列表,把它转换为Java的Locale,即获得了用户的Locale。大多数框架通常只返回权重最高的Locale

Spring MVC通过LocaleResolver来自动从HttpServletRequest中获取Locale。有多种LocaleResolver的实现类,其中最常用的是CookieLocaleResolver

package com.mszlu.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;

import java.util.Locale;
import java.util.TimeZone;

@Configuration
public class LagConfig {

    @Bean
    LocaleResolver createLocaleResolver() {
        CookieLocaleResolver clr = new CookieLocaleResolver();
        clr.setDefaultLocale(Locale.ENGLISH);
        clr.setDefaultTimeZone(TimeZone.getDefault());
        return clr;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

CookieLocaleResolverHttpServletRequest中获取Locale时,首先根据一个特定的Cookie判断是否指定了Locale,如果没有,就从HTTP头获取,如果还没有,就返回默认的Locale

当用户第一次访问网站时,CookieLocaleResolver只能从HTTP头获取Locale,即使用浏览器的默认语言。通常网站也允许用户自己选择语言,此时,CookieLocaleResolver就会把用户选择的语言存放到Cookie中,下一次访问时,就会返回用户上次选择的语言而不是浏览器默认语言。

4.2 提取资源文件

第二步是把写死在模板中的字符串以资源文件的方式存储在外部。对于多语言,主文件名如果命名为messages,那么资源文件必须按如下方式命名并放入classpath中:

  • 默认语言,文件名必须为messages.properties
  • 简体中文,Locale是zh_CN,文件名必须为messages_zh_CN.properties
  • 日文,Locale是ja_JP,文件名必须为messages_ja_JP.properties
  • 其它更多语言……

每个资源文件都有相同的key,例如,默认语言是英文,文件messages.properties内容如下:

language.select=Language
home=Home
signin=Sign In
copyright=Copyright©{0,number,##}
1
2
3
4

文件messages_zh_CN.properties内容如下:

language.select=语言
home=首页
signin=登录
copyright=版权所有©{0,number,##}
1
2
3
4

4.3 创建MessageSource

第三步是创建一个Spring提供的MessageSource实例,它自动读取所有的.properties文件,并提供一个统一接口来实现“翻译”:

// code, arguments, locale:
String text = messageSource.getMessage("signin", null, locale);
1
2

其中,signin是我们在.properties文件中定义的key,第二个参数是Object[]数组作为格式化时传入的参数,最后一个参数就是获取的用户Locale实例。

创建MessageSource如下:

@Bean("i18n")
MessageSource createMessageSource() {
    var messageSource = new ResourceBundleMessageSource();
    // 指定文件是UTF-8编码:
    messageSource.setDefaultEncoding("UTF-8");
    // 指定主文件名:
    messageSource.setBasename("messages");
    return messageSource;
}
1
2
3
4
5
6
7
8
9

注意到ResourceBundleMessageSource会自动根据主文件名自动把所有相关语言的资源文件都读进来。

再注意到Spring容器会创建不只一个MessageSource实例,我们自己创建的这个MessageSource是专门给页面国际化使用的,因此命名为i18n,不会与其它MessageSource实例冲突。

4.4 实现多语言

要在View中使用MessageSource加上Locale输出多语言,我们通过编写一个MvcInterceptor,把相关资源注入到ModelAndView中:

package com.mszlu.handler;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

@Component
public class MvcInterceptor implements HandlerInterceptor {
    @Autowired
    LocaleResolver localeResolver;

    // 注意注入的MessageSource名称是i18n:
    @Autowired
    @Qualifier("i18n")
    MessageSource messageSource;

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (modelAndView != null) {
            // 解析用户的Locale:
            Locale locale = localeResolver.resolveLocale(request);
            // 放入Model:
            modelAndView.addObject("__messageSource__", messageSource);
            modelAndView.addObject("__locale__", locale);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

不要忘了在WebMvcConfigurer中注册MvcInterceptor。现在,就可以在View中调用MessageSource.getMessage()方法来实现多语言:

package com.mszlu.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("lan")
public class LanController {


    @GetMapping("info")
    public ModelAndView info(){
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("/info.jsp");
        return modelAndView;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 <mvc:interceptors>

        <mvc:interceptor>
            <mvc:mapping path="/lan/**"/>
            <bean class="com.mszlu.handler.MvcInterceptor" />
        </mvc:interceptor>
    </mvc:interceptors>
1
2
3
4
5
6
7

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<a href="/signin">${ __messageSource__.getMessage('signin', null, __locale__) }</a>
</body>
</html>

1
2
3
4
5
6
7
8
9
10
11

4.5 切换Locale

最后,我们需要允许用户手动切换Locale,编写一个LocaleController来实现该功能:

package com.mszlu.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

@Controller
@RequestMapping("lan")
public class LanController {


    @GetMapping("info")
    public ModelAndView info(){
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("/info.jsp");
        return modelAndView;
    }

    @Autowired
    LocaleResolver localeResolver;

    @GetMapping("/locale/{lo}")
    public String setLocale(@PathVariable("lo") String lo, HttpServletRequest request, HttpServletResponse response) {
        // 根据传入的lo创建Locale实例:
        Locale locale = null;
        int pos = lo.indexOf('_');
        if (pos > 0) {
            String lang = lo.substring(0, pos);
            String country = lo.substring(pos + 1);
            locale = new Locale(lang, country);
        } else {
            locale = new Locale(lo);
        }
        // 设定此Locale:
        localeResolver.setLocale(request, response, locale);
        // 刷新页面:
        String referer = request.getHeader("Referer");
        return "redirect:" + (referer == null ? "/" : referer);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<%--
  Created by IntelliJ IDEA.
  User: lenovo
  Date: 2021/9/26
  Time: 16:53
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<a href="/signin">${ __messageSource__.getMessage('signin', null, __locale__) }</a>

<a href="/lan/locale/en_US">切换英文</a>
<a href="/lan/locale/zh_CN">切换中文</a>
</body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

message_en_US.properties:

language.select=Language
home=Home
signin=Sign In
copyright=Copyright?{0,number,##}
1
2
3
4

5. 定时任务

在很多应用程序中,经常需要执行定时任务。例如,每天或每月给用户发送账户汇总报表,定期检查并发送系统状态报告,等等

在spring的配置文件上,加上@EnableScheduling就开启了定时任务的支持

接下来,我们可以直接在一个Bean中编写一个public void无参数方法,然后加上@Scheduled注解:

package com.mszlu.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@EnableScheduling
public class TaskService {

    @Scheduled(cron = "0/5 * * * * *")
    public void checkSystemStatusEveryMinute() {
        System.out.println("Start check system status...");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上述注解指定了每隔5秒执行一次。现在,我们直接运行应用程序,就可以在控制台看到定时任务打印的日志:

Start check system status...
Start check system status...
1
2

5.1 cron 表达式

Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义

corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份

5.1.1 各字段的含义

字段允许值允许的特殊字符
秒(Seconds0~59的整数, - * / 四个字符
分(Minutes0~59的整数, - * / 四个字符
小时(Hours0~23的整数, - * / 四个字符
日期(DayofMonth1~31的整数(但是你需要考虑你月的天数),- * ? / L W C 八个字符
月份(Month1~12的整数或者 JAN-DEC, - * / 四个字符
星期(DayofWeek1~7的整数或者 SUN-SAT (1=SUN), - * ? / L C ## 八个字符
年(可选,留空)(Year1970~2099, - * / 四个字符

每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

(1)*:表示匹配该域的任意值。假如在Minutes域使用, 即表示每分钟都会触发事件。

(2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。

(3)-:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次

(4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.

(5),:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。

(6)L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。

(7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。

(8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。

(9)##:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4##2,表示某月的第二个星期三。

5.1.2 常用表达式例子

​ (1)0 0 2 1 * ? * 表示在每月的1日的凌晨2点执行任务

(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业

(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作

(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

(6)0 0 12 ? * WED 表示每个星期三中午12点

(7)0 0 12 * * ? 每天中午12点触发

(8)0 15 10 ? * * 每天上午10:15触发

(9)0 15 10 * * ? 每天上午10:15触发

(10)0 15 10 * * ? * 每天上午10:15触发

(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发

(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发

(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发

(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发

(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发

(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发

(18)0 15 10 15 * ? 每月15日上午10:15触发

(19)0 15 10 L * ? 每月最后一日的上午10:15触发

(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发

(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发

(22)0 15 10 ? * 6##3 每月的第三个星期五上午10:15触发

6. 日志

日志必不可少,在开发的时候,我们应该在获取参数,获取关键结果,可能出问题的地方,尽量打印一些日志,这样发生问题的时候,可以通过日志快速的定位问题或者还原现场

6.1 Log4j和Logback

现在市面上,主流的日志有log4j,logback,log4j2 ,log4j和logback是同一个作者开发的,log4j2是apache开发的,性能上log4j2 > logback > log4j。

log4j已经停止维护,主流使用的日志是logback和log4j2,logback市面上使用更为广泛一些

6.2 Logback

引入依赖:

 <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.3</version>
        </dependency>
        <dependency>
            <groupId>org.logback-extensions</groupId>
            <artifactId>logback-ext-spring</artifactId>
            <version>0.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.12</version>
        </dependency>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. 第一个logback-classic包含了logback本身所需的slf4j-api.jarlogback-core.jarlogback-classsic.jar
  2. 第二个logback-ext-spring是由官方提供的对Spring的支持
  3. 第三个jcl-over-slf4j是用来把Spring源代码中大量使用到的commons-logging替换成slf4j,只有在添加了这个依赖之后才能看到Spring框架本身打印的日志–即info文件中打印出的spring启动日志信息,否则只能看到开发者自己打印的日志。

6.2.1 引入配置文件

在resources下创建logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--scan当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。scanPeriod 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。debug 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。-->
<configuration scan="true" scanPeriod="60 seconds" debug="true">  
    <!-- 模块名称, 影响日志配置名,日志文件名 --> 
    <property name="appName" value="mszluSpring"/>
    <property name="logMaxSize" valule="100MB"/>
    <!--rootPath 日志路径 -->  
    <property name="rootPath" value="D:/git/spring-logback/logs"/>
    <contextName>${appName}</contextName>

   <!--控制台输出 --> 
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss}|%t|%p| %m |%logger:%L%n</pattern>
        </encoder>
    </appender>

	<!--将过滤器的日志级别配置为DEBUG,所有DEBUG级别的日志交给appender处理,非DEBUG级别的日志,被过滤掉。-->
    <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!-- 设置日志不超过${logMaxSize}时的保存路径-->
        <file>${rootPath}/${appName}/debug/${appName}-dlog.log</file>
        <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。-->  
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${rootPath}/${appName}/debug/${appName}-dlog-%d{yyyy-MM-dd}-%d.log</fileNamePattern>
            <maxHistory>30</maxHistory>
             <!-- 当天的日志大小 超过${logMaxSize}时,压缩日志并保存 --> 
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
         <!-- 日志输出的文件的格式  -->  
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss}|%t|%p| %m |%logger:%L%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!--过滤器 多个过滤器时,按照配置顺序执行-->
        <!--级别过滤器 执行一个过滤器会有返回个枚举值,即DENY,NEUTRAL,ACCEPT其中之一-->
        <!--DENY,日志将立即被抛弃不再经过其他过滤器-->
        <!--NEUTRAL,有序列表里的下个过滤器过接着处理日志-->
        <!--ACCEPT,日志会被立即处理,不再经过剩余过滤器。-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--设置过滤级别-->
            <level>DEBUG</level>
            <!--用于配置符合过滤条件的操作-->
            <onMatch>ACCEPT</onMatch>
            <!--用于配置不符合过滤条件的操作-->
            <onMismatch>DENY</onMismatch>
        </filter>
        <!--临界值过滤器 过滤掉低于指定临界值的日志。当日志级别等于或高于临界值时,过滤器返回NEUTRAL;当日志级别低于临界值时,日志会被拒绝。-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!--只记录 DEBUG 级别的日志 -->
            <level>DEBUG</level>
        </filter>
    </appender>


    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${rootPath}/${appName}/info/${appName}-ilog.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${rootPath}/${appName}/all/${appName}-ilog-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss}|%t|%p| %m |%logger:%L%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>



    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${rootPath}/${appName}/error/${appName}-elog.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${rootPath}/${appName}/all/${appName}-elog-%d{yyyy-MM-dd}-%e.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss}|%t|%p| %m |%logger:%L%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender> -->  
<!--     <logger name="com.mszlu" additivity="false">   -->
<!--         <level value="debug" />   -->
<!--         <appender-ref ref="stdout" />   -->
<!--         <appender-ref ref="file" />   -->
<!--     </logger>   -->
    <!--name用来指定受此loger约束的某一个包或者具体的某一个类-->
    <!--level 用来设置打印日志级别,大小写无关-->
    <!--addtivity:是否向上级loger传递打印信息。默认是true。-->
    <!--<logger>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger-->
    <logger name="jdbc" level="INFO"/>
    <logger name="org" level="INFO"/>
    <logger name="net" level="INFO"/>
    <logger name="sql" level="INFO"/>
    <logger name="java.sql" level="INFO"/>
    <logger name="javax" level="INFO"/>

    <!--日志的输出级别由低到高(越来问题越严重)trace->debug->info->warn->error -->
    <!-- root将级别为DEBUG及大于DEBUG的日志信息交给已经配置好的name='STDOUT'的appender处理,将信息打印到控制台-Console -->  
    <!--root 也是logger,只不过是根logger,没有name属性,指定为root-->
    <root level="DEBUG">
    <!-- appender-ref标识这个appender将会添加到本应用的日志系统中 -->
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="INFO"/>
        <appender-ref ref="DEBUG"/>
        <appender-ref ref="ERROR"/>
    </root>    


</configuration>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

6.2.2 日志级别

分为OFFFATALERRORWARNINFODEBUGTRACEALL或者您定义的级别。

OFF: 关闭:最高级别,不打印日志。

FATAL : 致命:指明非常严重的可能会导致应用终止执行错误事件。

ERROR : 错误:指明错误事件,但应用可能还能继续运行。

WARN : 警告:指明可能潜在的危险状况。

INFO : 信息:指明描述信息,从粗粒度上描述了应用运行过程。

DEBUG : 调试:指明细致的事件信息,对调试应用最有用。

TRACE : 跟踪:指明程序运行轨迹,比DEBUG级别的粒度更细。

ALL:所有:所有日志级别,包括定制级别。

日志优先级别标准顺序为:

ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

如果定义日志级别为INFO,则不打印DEBUG和TRACE的日志