spring framework boot
👍 web api 框架
2022/5/26 10:25:37
➡️

spring boot

url 路径匹配

精确匹配

@RestController
@RequestMapping("/home")
public class HomeController {
  @RequestMapping(value = "/index/user",method = RequestMethod.GET)
   public String index(){
     return "index";
   }
}

通配符匹配

  • * 通配一个组词组
  • ** 通配任意路径
  • ? 通配一个字符

优先级: 无通配符的 > ? > * > **

@RestController
@RequestMapping("/home")
public class HomeController {

  @RequestMapping(value = "/index/bar",method = RequestMethod.GET)
  public String index1(){
    return "index1";
  }

  @RequestMapping(value = "/index/*",method = RequestMethod.GET)
  public String index2(){
    return "index2";
  }


  @RequestMapping(value = "/index/**",method = RequestMethod.GET)
  public String index3(){
    return "index3";
  }

  @RequestMapping(value = "/index/?",method = RequestMethod.GET)
  public String index4(){
    return "index4";
  }
}

params和header参数匹配

params:

  1. params={"username"},存在 username 参数时通过。
  2. params={"password=123"},参数 password 等于 123时通过。
  3. params={"password!=123"},参数 password 等于 123时通过。
  4. params={"!username"},不存在 username 通过。

header

  1. headers={"access"} 存在 access 参数时通过
  2. headers={"token=token1"} 参数 token 等于 token1 时通过。
@RestController
@RequestMapping("/home")
public class HomeController {
  //http://localhost:8080/home/index/bar?username=&password=123
  @RequestMapping(value = "/index/bar",method = RequestMethod.GET,
  params={"username","password=123"} , headers = {"access","token=token1"} )
  public String index1(@RequestParam(value = "username") String username , 
  @RequestParam("password") int password, @RequestHeader("token1") String token){
    return "index1,"+username+","+password+","+token;
    //index1,,123,token1
  }
}

URL变量-正则表达式匹配

虽然@RequestMapping路由支持URL变量,但是很多时候需要对URL变量进行更加精确的定义和限制 例如用户名只包含字母

@RestController
@RequestMapping("/home")
public class HomeController {
  //http://localhost:8080/home/index/bar/dsa   匹配
  //http://localhost:8080/home/index/bar/dsa1 不匹配
  @RequestMapping(value = "/index/bar/{userName:[a-zA-Z]+}",method = RequestMethod.GET )
  public String index1(@PathVariable String userName){
    return "index1,"+userName;
    //index1,dsa
  }
}

参数传递

@PathVariable,@RequestParam, @RequestHeader

@PathVariable,在Web应用中,最常用的参数传递方式就是URL传参,也就是将参数放在请求的URL中 @RequestMapping注解使用{}来声明URL变量,通过 @PathVariable 来绑定变量

@RestController
@RequestMapping("/home")
public class HomeController {
  @RequestMapping(value = "/index/{id}",method = RequestMethod.GET)
   //如果路径里的参数名和方法的参数名一样,可以 index(@PathVariable int id)
   public String index(@PathVariable("id") int id){
     return "return"+id;
   }
}

表单数据

//表单字段
@Data
class  Order{
  private int id;
  private String name;
}

@RestController
@RequestMapping("/home")
public class HomeController {
  
  //http://localhost:8080/home/index/bar
  @RequestMapping(value = "/index/bar",method = RequestMethod.POST)
  //接收表单,通过对应的类
  public String index1(Order order){
       return order.toString();
       //Order(id=1, name=bob)
  }
}

@RequsetBody 接收 JSON 数据

@Data
class  Order{
  private int id;
  private String name;
}

@RestController
@RequestMapping("/home")
public class HomeController {
  
  /*
   json
   http://localhost:8080/home/index/bar

   json
   {
     "id": 1,
     "name": "bob"
   }

   发送集合
   [
     {
     "id": 1,
     "name": "bob"
     }
   ]
  */
  @RequestMapping(value = "/index/bar",method = RequestMethod.POST)
  //接收 json 数据
  public String index1(@RequestBody Order order){
       return order.toString();
  }
}

通过 HttpServletRequest 手动获取

@RestController
@RequestMapping("/home")
public class HomeController {
  @RequestMapping(value = "/index/bar",method = RequestMethod.POST)
  //接收 json 数据
  public Object index1(HttpServletRequest request){
       return request.getRequestURI();
  }
}

spring boot controller拦截器

拦截器是通过 aop 动态生成的,并不依赖 servlet,只能对 Controller 请求起作用

HandlerInterceptor

HandlerInterceptor的场景:

  • 鉴权,可以使用HandlerInterceptor 但是Filter是更佳的选择。
  • 审计日志,记录每一个请求。
  • Token 解析或校验。
  • Handler(方法) 执行时间统计。
public interface HandlerInterceptor {

  //在方法被调用前执行,在该方法中可以做类似校验的功能。如果返回true,则继续调用下一个拦截器。如果返回false,则中断执行,也就是说我们想调用的方法不会被执行,
  //但是可以修改response为你想要的响应。
  default boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
  Object handler) throws Exception {
    return true;
  }
  
  //在方法执行后调用。如果是传统的 mvc ,是在 调用试图前执行
  default void postHandle(HttpServletRequest request, HttpServletResponse response, 
  Object handler, @Nullable ModelAndView modelAndView) throws Exception {
  }
  
  //在视图渲染完毕或者调用方已经拿到响应后执行
  default void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
  Object handler, @Nullable Exception ex) throws Exception {
  }
}

拦截器(Interceptor)是 Spring MVC 提供的一种强大的功能组件。它可以对用户请求进行拦截,并在请求进入控制器(Controller)之前、控制器处理完请求后、甚至是渲染视图后,执行一些指定的操作。 在 Spring MVC 中,拦截器的作用与 Servlet 中的过滤器类似,它主要用于拦截用户请求并做相应的处理,例如通过拦截器,我们可以执行权限验证、记录请求信息日志、判断用户是否已登录等操作。 Spring MVC 拦截器使用的是可插拔式的设计,如果我们需要某一拦截器,只需在配置文件或代码中中启用该拦截器即可;如果不需要这个拦截器,则只要在配置文件或代码中取消应用该拦截器即可。

日志记录统计方法耗时

// controller
@RestController
@RequestMapping("/home")
public class HomeController {
  @RequestMapping(value = "/index1",method = RequestMethod.POST)
  public Object index1(HttpServletRequest request){
       return request.getRequestURI()+ LocalDateTime.now();
  }

  @RequestMapping(value = "/index2",method = RequestMethod.GET)
  public Object index2(HttpServletRequest request){
    return request.getRequestURI()+ LocalDateTime.now();
  }
}


//日志拦截器
@Component
public class MyHandlerInterceptor implements HandlerInterceptor {
  private static NamedThreadLocal<Long> startTimeThreadLocal =
  new NamedThreadLocal<Long>("startTimeThreadLocal");
  @Override
  public boolean preHandle(HttpServletRequest request, 
  HttpServletResponse response, Object handler) throws Exception {
    if(handler instanceof HandlerMethod){
        //给线程本地变量设置当前的毫秒数
        startTimeThreadLocal.set(System.currentTimeMillis());
        var handlerMethod=(HandlerMethod)handler;

        var  stringBuilder=new StringBuilder();
        stringBuilder.append("ThreadName :"+Thread.currentThread().getName()).append("\n");
        stringBuilder.append("StatTime :"+new SimpleDateFormat("yyyy/MM/dd hh:mm:ss").
        format(System.currentTimeMillis())).append("\n");
        stringBuilder.append("URL :"+request.getRequestURI()).
        append(";method:"+request.getMethod()).append("\n");
        stringBuilder.append("Controller :"+handlerMethod.
        getBeanType().getName()).append("\n");
        stringBuilder.append("Method :"+handlerMethod.getMethod().getName()).append("\n");
        System.out.println(stringBuilder.toString());

    }
    //返回值:true表示继续流程
    return true;
  }
  
  //如果不是传统的 mvc,这个方法用不到
  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
   ModelAndView modelAndView) throws Exception {

  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
  Object handler, Exception ex) throws Exception {
      if(handler instanceof HandlerMethod){
        var handlerMethod=(HandlerMethod)handler;
        var startTime =startTimeThreadLocal.get();
        startTimeThreadLocal.remove();
        var consume=System.currentTimeMillis()-startTime;
        var  stringBuilder=new StringBuilder();
        stringBuilder.append("ThreadName :"+Thread.currentThread().getName()).append("\n");
        stringBuilder.append("URL :"+request.getRequestURI()).
        append(";method:"+request.getMethod()).append("\n");
        stringBuilder.append("Controller :"+handlerMethod.getBeanType().getName()).append("\n");
        stringBuilder.append("Method :"+handlerMethod.getMethod().getName()).append("\n");
        stringBuilder.append("Consume :"+consume+"ms").append("\n");
        System.out.println(stringBuilder.toString());
    }
  }
}

拦截器输出
/*
ThreadName :http-nio-8080-exec-5
StatTime :2022/11/24 10:43:02
URL :/home/index1;method:POST
Controller :com.hank.springbootdemo1.controller.HomeController
Method :index1

ThreadName :http-nio-8080-exec-5
URL :/home/index1;method:POST
Controller :com.hank.springbootdemo1.controller.HomeController
Consume :1ms

----------------------------------------------

ThreadName :http-nio-8080-exec-6
StatTime :2022/11/24 10:43:07
URL :/home/index2;method:GET
Controller :com.hank.springbootdemo1.controller.HomeController
Method :index2

ThreadName :http-nio-8080-exec-6
URL :/home/index2;method:GET
Controller :com.hank.springbootdemo1.controller.HomeController
Consume :1ms
*/

自定义的拦截器必须要注册,才能起作用

//注册重写 WebMvcConfigurer 的 addInterceptors 方法
@Component
public class MyMvcConfig implements WebMvcConfigurer {
  private MyHandlerInterceptor myHandlerInterceptor;    
  public MyMvcConfig(MyHandlerInterceptor myHandlerInterceptor, 
  MyHandlerInterceptor2 myHandlerInterceptor2) {
    this.myHandlerInterceptor = myHandlerInterceptor;
  
  }
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
     //addPathPatterns 拦截器通过 addPathPatterns 能拦截的url,拦截器通过 
     //excludePathPatterns 指定不能拦截的 url 路径
    registry.addInterceptor(myHandlerInterceptor).addPathPatterns("/**").excludePathPatterns("");
  }
}

在 controller 方法里抛出异常,上面的拦截器无法处理异常,只能记录一下,需要使用异常拦截器去处理

public Object index1(HttpServletRequest request){
       var i=1/0;
       return request.getRequestURI()+ LocalDateTime.now();
}

HandlerExceptionResolver -全局异常拦截器

Spring MVC 遵循正常流程与异常处理分离策略。只需关心正常逻辑,由框架对异常流程进行统一处理,HandlerExceptionResolver 在其他拦截器执行前对异常进行处理,DispatcherServlet 中用于处理异常恢复的组件

注意点:

  • 可以有多个 HandlerExceptionResolver 处理器 多个 HandlerExceptionResolver 在 Chain 中的顺序,由 Order 接口或 @Order 注解进行控制(实现 Order 接口,或通过 @Order 进行注解以描述顺序)。顺序在自定义 HandlerExceptionResolver 时至关重要,需要将明确的、处理范围小的 Resolver 放在链的头部。
  • HandlerExceptionResolver 返回值:
    • ModelAndView,但 ModelAndView#isEmpty()(视图、数据都为空格)为 true,说明异常已经被成功处理,但并不使用视图进行结果返回,一般用于处理 Rest 风格请求。
    • ModelAndView,但 ModelAndView#isEmpty() 为 false,说明异常已经被成功处理,并通过视图返回,一般用于返回异常页面,如 404,5xx 等
    • 为 null,说明异常没有被处理,将尝试下一个 HandlerExceptionResolver

自定义的异常类

@Data
public class BusinessException extends RuntimeException {
  /**
   * 异常处理码
   */
  private final int code;
  /**
   * 异常消息
   */
  private final String msg;
  
  private final String timestamp = new SimpleDateFormat("yyy-mm-dd hh:mm:ss").
  format(System.currentTimeMillis());

  public BusinessException(int code, String msg){
    this.code = code;
    this.msg = msg;
  }
  public BusinessException(int code, String msg, Exception e) {
    super(e);
    this.code = code;
    this.msg = msg;
  }
}

统一返回的异常类

@Data
public class RestResult<T> {
  public static final int ERROR = -1;
  public static final int FAIL = 0;
  public static final int SUCCESS = 1;
  private int status;
  private String message;
  private final String timestamp = new SimpleDateFormat("yyy-mm-dd hh:mm:ss").
  format(System.currentTimeMillis());
  private T data;

  /**
   * 成功
   * @param t
   * @param <T>
   * @return
   */
  public static  <T> RestResult<T> success(T t){
    RestResult<T> response = new RestResult<>();
    response.setStatus(SUCCESS);
    response.setMessage("SUCCESS");
    response.setData(t);
    return response;
  }

  /**
   * 异常
   * @param e
   * @param <T>
   * @return
   */
  public static  <T> RestResult<T> error(BusinessException e){
    RestResult<T> response = new RestResult<>();
    response.setStatus(e.getCode());
    response.setMessage(e.getMsg());
    response.setData(null);
    return response;
  }

  /**
   * 异常
   * @param e
   * @param <T>
   * @return
   */
  public static <T> RestResult<T> error(Throwable e){
    RestResult<T> response = new RestResult<>();
    response.setStatus(ERROR);
    response.setMessage(e.toString());
    response.setData(null);
    return response;
  }

  /**
   * 失败
   * @param msg
   * @param <T>
   * @return
   */
  public static  <T> RestResult<T> fail(String msg){
    RestResult<T> response = new RestResult<>();
    response.setStatus(FAIL);
    response.setMessage(msg);
    return response;
  }
}

全局异常处理类,抛出的异常会在执行 HandlerInterceptor 的方法前处理

@Component
public class RestHandlerExceptionResolver implements HandlerExceptionResolver {
  @Override
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, 
  Object handler, Exception ex) {
    // 是 Rest 请求 并且能够处理
    if(isRestRequest(handler) && isAcceptException(ex)){
      // 将异常传化为 RestResVo 对象
      RestResult<Void> restResVo = RestResult.error((BusinessException)ex);
      try {
        //可以做错误处理日志记录
        // 以 Json 格式进行写回
        ObjectMapper objectMapper = new ObjectMapper();
        response.getWriter().println(objectMapper.writeValueAsString(restResVo));
      }catch (Exception e){
      }
      // empty ModelAndView 表示错误已经处理
      return new ModelAndView();
    }
    // 表示错误没有处理继续抛出
    return null;
  }

  //是 restFull api
  private boolean isRestRequest(Object handler) {
    if (handler instanceof HandlerMethod){
      HandlerMethod handlerMethod = (HandlerMethod) handler;
      return AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResponseBody.class) !=null ||
          AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), ResponseBody.class) != null ||
          AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), RestController.class) != null;
    }
    return false;
  }
   
  //抛出的异常必须是 BusinessException 类型的异常
  private boolean isAcceptException(Exception ex) {
    return ex instanceof BusinessException;
  }
}

测试代码

@RestController
@RequestMapping(value = "/home")
public class HomeController {
  @RequestMapping(value = "/index1",method = RequestMethod.POST)
  public Object index1(HttpServletRequest request){
    try {
      var i=1/0;
    }catch(Exception e){
       throw new BusinessException(-1,"server inner error",e);
    }
    return request.getRequestURI()+ LocalDateTime.now();
  }
}

/*
{
    "status": -1,
    "message": "server inner error",
    "timestamp": "2022-50-24 03:50:09",
    "data": null
}
*/

@ExceptionHandler-局部异常处理

ExceptionHandler 方法只能存在于 Controller 和 ControllerAdvice 类中的方法中,只对当前 Controller 中所抛出的异常进行处理,主要用于精细化的异常控制* 在处理业务流程出现异常时,Spring MVC 的异常处理体系会优先使用该 Controller 中的 ExceptionHandler。如果异常类型匹配成功,则直接进入异常处理流程。

@RestController
@RequestMapping(value = "/home")
public class HomeController {
  @RequestMapping(value = "/index1",method = RequestMethod.POST)
  public String index1(HttpServletRequest request){
    try {
      var i=1/0;
    }catch(Exception e){
       throw new BusinessException(-1,"server inner error",e);
    }
    return request.getRequestURI()+ LocalDateTime.now();
  }
  
  //只处理当前的 controller 的 handler 的错误
  //如果错误被处理了,全局异常处理器就不能处理了
  @ExceptionHandler({BusinessException.class})
  @ResponseBody
  @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) // 知道异常错误码
  public  RestResult doSome(Exception ex){
     var result=  RestResult.error((BusinessException)ex);
     result.setMessage("ExceptionHandler 处理的错误");
     return result;
  }
}

多个 HandlerInterceptor 拦截器的执行顺序

注意点

  • 多个 HandlerInterceptor 拦截器,拦截器的 preHandle 方法返回值决定后续的的拦截器是否执行
  • 拦截器的执行顺序取决于注册顺序

使用 HandlerExceptionResolver 进行全局异常拦截,controller 的 handler 没有抛出异常

graph TB preHandle1(MyHandlerInterceptorpreHandle1-preHandle) postHandle1(MyHandlerInterceptorpreHandle1-postHandle) afterCompletion1(MyHandlerInterceptorpreHandle1-afterCompletion) style preHandle1 fill: #f9f, stroke: #333, stroke-width: 1px; style postHandle1 fill: #f9f, stroke: #333, stroke-width: 1px; style afterCompletion1 fill: #f9f, stroke: #333, stroke-width: 1px; preHandle2([MyHandlerInterceptorpreHandle2-preHandle]) postHandle2([MyHandlerInterceptorpreHandle2-postHandle]) afterCompletion2([MyHandlerInterceptorpreHandle2-afterCompletion]) style preHandle2 fill: #fe9f, stroke: #333, stroke-width: 1px; style postHandle2 fill: #fe9f, stroke: #333, stroke-width: 1px; style afterCompletion2 fill: #fe9f, stroke: #333, stroke-width: 1px; style handler1 fill: #fdf, stroke: #333, stroke-width: 1px; style handler2 fill: #fdf, stroke: #333, stroke-width: 1px; style handler3 fill: #fdf, stroke: #333, stroke-width: 1px; preHandle1--> preHandle2 preHandle2--> handler1 preHandle2--> handler2 preHandle2--> handler3 handler1 --> postHandle2 handler2 --> postHandle2 handler3 --> postHandle2 postHandle2 --> postHandle1 postHandle1 --> afterCompletion2 afterCompletion2 --> afterCompletion1

使用 HandlerExceptionResolver 进行全局异常拦截,controller 的 handler 抛出异常

graph TB preHandle1(MyHandlerInterceptorpreHandle1-preHandle) afterCompletion1(MyHandlerInterceptorpreHandle1-afterCompletion) style preHandle1 fill: #f9f, stroke: #333, stroke-width: 1px; style afterCompletion1 fill: #f9f, stroke: #333, stroke-width: 1px; preHandle2([MyHandlerInterceptorpreHandle2-preHandle]) afterCompletion2([MyHandlerInterceptorpreHandle2-afterCompletion]) style preHandle2 fill: #fe9f, stroke: #333, stroke-width: 1px; style afterCompletion2 fill: #fe9f, stroke: #333, stroke-width: 1px; style handler1 fill: #fdf, stroke: #333, stroke-width: 1px; style handler2 fill: #fdf, stroke: #333, stroke-width: 1px; style handler3 fill: #fdf, stroke: #333, stroke-width: 1px; preHandle1--> preHandle2 preHandle2--> handler1 preHandle2--> handler2 preHandle2--> handler3 handler1 --> afterCompletion2 handler2 --> afterCompletion2 handler3 --> afterCompletion2 afterCompletion2 --> afterCompletion1

@Component
public class MyHandlerInterceptor1 implements AsyncHandlerInterceptor {
  private static NamedThreadLocal<Long> startTimeThreadLocal =
  new NamedThreadLocal<Long>("startTimeThreadLocal");
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
  Object handler) throws Exception {
    System.out.println("MyHandlerInterceptor1-preHandle");
    return true;
  }

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

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

@Component
public class MyHandlerInterceptor2 implements AsyncHandlerInterceptor {
  private static NamedThreadLocal<Long> startTimeThreadLocal =
  new NamedThreadLocal<Long>("startTimeThreadLocal");
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
   Object handler) throws Exception {
    System.out.println("MyHandlerInterceptor2-preHandle");
    return true;
  }

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

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

//注册的顺序就是执行的顺序
@Component
public class MyMvcConfig implements WebMvcConfigurer {
  private MyHandlerInterceptor1 myHandlerInterceptor1;
  private MyHandlerInterceptor2 myHandlerInterceptor2;

  public MyMvcConfig(MyHandlerInterceptor1 myHandlerInterceptor1, 
  MyHandlerInterceptor2 myHandlerInterceptor2) {

    this.myHandlerInterceptor1 = myHandlerInterceptor1;
    this.myHandlerInterceptor2 = myHandlerInterceptor2;
  }
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(myHandlerInterceptor1).addPathPatterns("/**").excludePathPatterns("");
    registry.addInterceptor(myHandlerInterceptor2).addPathPatterns("/**").excludePathPatterns("");
  }
}

Servlet 过滤器

过滤器是Java Servlet规范中定义的,能够在HTTP请求发送给Servlet之前对Request(请求)和Response(返回)进行检查和修改,从而起到过滤的作用。通过对Web服务器管理的所有Web资源(如JSP、Servlet、静态图片文件或静态HTML文件等)过滤

过滤器和拦截器的功能类似,但技术实现差距比较大,两者的区别包括以下几个方面:

  • 过滤器依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用。
  • 过滤器的执行由Servlet容器回调完成,而拦截器通常通过动态代理的方式来执行。
  • 过滤器内不能获取ioc 容器内的 bean ,以为过滤器实例化的时候, spring ioc 容器还没有被实例化

功能:

  • 实现URL级别的权限访问控制
  • 过滤敏感词汇
  • 排除有XSS威胁的字符
  • 压缩响应信息等一些高级功能等

过滤器可以定义多个,按照过滤器链顺序调用,可以通过 Ordered 接口或注解 @Order 或编程式指定,拦截器和过滤器的执行顺序图:

graph BT subgraph 拦截器链 Interceptors1(Interceptors1) -.request.-> Interceptors2(Interceptors2)-.request.-> Interceptors3(Interceptors3) -.response.-> Interceptors2(Interceptors2) -.response.-> Interceptors1(Interceptors1) end style 拦截器链 fill: #fdf, stroke: #333, stroke-width: 1px; subgraph 过滤器链 Filter1(Filter1) -.request.-> Filter2(Filter2)-.request.->Filter3(Filter3) -.response.-> Filter2(Filter2) -.response.-> Filter1(Filter1) end style 过滤器链 fill: #f3f, stroke: #333, stroke-width: 1px; subgraph servlet-dispatcherServlet Filter3 -.request.-> dispatcherServlet dispatcherServlet -.response.-> Filter3 dispatcherServlet -.request.->Interceptors1 Interceptors1 -.response.->dispatcherServlet end style servlet-dispatcherServlet fill: green, stroke: #333, stroke-width: 1px; subgraph http访问 http -.request.-> Filter1 Filter1 -.response.->http end subgraph controller handler controller -.request.-> Interceptors3 Interceptors3 -.response.-> controller end

public interface Filter {当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,在过滤器 Filter 的整个生命周期也只会被调用一次

  //该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次。注意:这个方法必须执行成功,否则过滤器会不起作用。
  default void init(FilterConfig filterConfig) throws ServletException {
  }

  // 容器中的每一次请求都会调用该方法, FilterChain 用来调用下一个过滤器 Filter。
  void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
  
  //当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,在过滤器 Filter 的整个生命周期也只会被调用一次
  default void destroy() {
  }
}

@WebFilter 注解注册过滤器

@WebFilter 和 @ServletComponentScan 配合,此注解是定义在 javax.servlet.annotation 包里,属于 servlet 的一部分

注意点:

  1. @WebFilter 写的过滤器 需要 @ServletComponentScan 配合,urlPatterns 才能起作用
  2. @WebFilter 写的过滤器 通过 @Component 标注了 urlPatterns 会失效,会对所有的 url 起作用

注解可以使用的属性:

属性名 类型 默认值 作用
filterName String “” 指定过滤器的 name 属性
value String[] {} 该属性等价于 urlPatterns 属性。两者不应该同时使用
urlPatterns String[] {} 指定一组过滤器的 URL 匹配模式
servletNames String[] {} 指定过滤器将应用于哪些 Servlet。取值是 @WebServlet 中的 name 属性的取值,或者是 web.xml 中的取值
dispatcherTypes DispatcherType DispatcherType.REQUEST 指定过滤器的转发模式。具体取值包括:ASYNC、ERROR、FORWARD 、
initParams WebInitParam[] {} 指定一组过滤器初始化参数
asyncSupported boolean false 声明过滤器是否支持异步操作模式
description String “” 该过滤器的描述信息
displayName String “” 该过滤器的显示名,通常配合工具使用

过滤器实现解压缩请求信息和压缩响应信息

/**
 * describe:
 * 注解实现的过滤器,initParams 设置参数
 * @author: Hbai
 * @date: 2022/11/26 18:33
 */
@Order(1)
@WebFilter(filterName = "myFilter",urlPatterns = "/home/index1",initParams = 
{@WebInitParam(name = "encoding",value = "UTF-8")})
public class MyFilter implements Filter {
  private  String encoding;
  @Override
  public void init(FilterConfig filterConfig) {
     encoding=filterConfig.getInitParameter("encoding");
  }

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, 
  FilterChain filterChain) throws IOException, ServletException {
     //对请求的数据进行解压缩,对返回的数据进行压缩
    var httpServletRequestWrapper  =new MyHttpServletRequestWrapper(
      (HttpServletRequest) servletRequest);
    //对解压后的内容指定格式, 要注意这里写的要是小写,是个坑
    httpServletRequestWrapper.addHeader(HttpHeaders.CONTENT_TYPE.
    toLowerCase(),"application/json");

    //设置编码
    servletResponse.setCharacterEncoding(encoding);
    var httpServletResponseWrapper=
    new MyHttpServletResponseWrapper((HttpServletResponse) servletResponse);

    var acceptEncoding = 
    ((HttpServletRequest)servletRequest).getHeader(HttpHeaders.ACCEPT_ENCODING);
    var isReturnGzip=
    StringUtils.hasLength(acceptEncoding) && acceptEncoding.indexOf("gzip")!=-1;
    filterChain.doFilter(httpServletRequestWrapper,
    isReturnGzip?httpServletResponseWrapper:servletResponse );

    //输出内容到浏览器
    if(isReturnGzip) httpServletResponseWrapper.finish();
  }

  @Override
  public void destroy() {
    System.out.println("AFilter 后置");
  }
}

//对 HttpServletRequest 只能读取,不能改写,实现 HttpServletRequestWrapper 进行改写
class MyHttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper{

  private final HttpServletRequest request;

  //需要改写头部
  private Map<String, String> headerMap = new HashMap<>();

  public MyHttpServletRequestWrapper(HttpServletRequest request) {
     super(request);
     this.request=request;
  }
  
  /解压缩请求流,转换数据为 json 数据
  @Override
  public ServletInputStream getInputStream() throws IOException {
       var  stream= request.getInputStream();
       var contentEncoding = request.getHeader(HttpHeaders.CONTENT_ENCODING);
       if(StringUtils.hasLength(contentEncoding) && contentEncoding.indexOf("gzip") !=-1 ){
           var gzipInputStream = new GZIPInputStream(stream);
           var bout = new ByteArrayOutputStream();
           int len = -1;
           var buffer = new byte[128];
           while((len = gzipInputStream.read(buffer))>0){
               bout.write(buffer, 0, len);
           }
           var bInput=new ByteArrayInputStream(bout.toByteArray());

           var  servletInputStream = new ServletInputStream() {
             @Override
             public boolean isFinished() {
               return false;
             }

             @Override
             public boolean isReady() {
               return false;
             }

             @Override
             public void setReadListener(ReadListener readListener) {

             }

             //读取字节
             @Override
             public int read() throws IOException {
               return bInput.read();
             }
           };
           return servletInputStream;
       }
      return stream;
  }
 
  /**
   * @param name
   * @param value
   */
  public void addHeader(String name, String value) {
    headerMap.put(name, value);
  }

  @Override
  public String getHeader(String name) {
    var value = super.getHeader(name);
    if(headerMap.containsKey(name)){
      value=headerMap.get(name);
    }
    return  value;
  }

  @Override
  public Enumeration<String> getHeaders(String name) {
    var values = Collections.list(super.getHeaders(name)) ;
    if(headerMap.containsKey(name)){
      var i=headerMap.get(name);
       values=new  ArrayList<String>(){{add(i);}};
    }
    return Collections.enumeration(values);
  }

  @Override
  public Enumeration<String> getHeaderNames() {
    var names = Collections.list(super.getHeaderNames()) ;
    headerMap.keySet().forEach(p->{
      if(!names.contains(p)){
        names.add(p);
      }
    });
    return Collections.enumeration(names);
  }
}
/**
 * getWriter() 该方法用于返回Servlet引擎创建的字符输出流对象,Servlet程序可以按字符形式输出响应正文。
 * getOutputStream()  该方法用于返回Servlet引擎创建的字节输出流对象,Servlet程序可以按字节形式输出响应正文。
 * 注意:
 * 1. getOutputStream()和getWriter()这两个方法互相排斥,调用了其中的任何一个方法后,就不能再调用另一方法。
 * 2. getOutputStream()返回的字节输出流对象,类型为:ServletOutputStream,直接输出字节数组中的二进制数据。
 * 3. getWriter()方法将Servlet引擎的数据缓冲区包装成PrintWriter类型的字符输出流对象后返回,PrintWriter对象只能输出字符文本内容。
 */

// 通过继承 HttpServletResponseWrapper 实现改写响应流
class MyHttpServletResponseWrapper extends HttpServletResponseWrapper {
  private GZIPResponseStream gzipStream;
  private ServletOutputStream outputStream;

  public MyHttpServletResponseWrapper(HttpServletResponse response) {
    super(response);
    //告知客户端,返回的数据是 gzip
    response.addHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
  }

  //关闭流,才能输出
  public void finish() throws IOException {
    if (outputStream != null) {
      outputStream.close();
    }
    if (gzipStream != null) {
      gzipStream.close();
    }
  }

  @Override
  public ServletOutputStream getOutputStream() throws IOException {
    if (outputStream == null) {
      initGzip();
      outputStream = gzipStream;
    }
    return outputStream;
  }



  /* Creates a new output stream for writing compressed data in
   * the GZIP file format. */
  private void initGzip() throws IOException {
    gzipStream = new GZIPResponseStream(getResponse().getOutputStream());
  }
}

/**
 * 写入的字节流变为压缩流 
 */

class GZIPResponseStream extends ServletOutputStream {
  GZIPOutputStream gzipStream;
  final AtomicBoolean open = new AtomicBoolean(true);

  public GZIPResponseStream(OutputStream output) throws IOException {
    gzipStream = new GZIPOutputStream(output);
  }

  @Override
  public void close() throws IOException {
    if (open.compareAndSet(true, false)) {
      gzipStream.close();
    }
  }

  @Override
  public void flush() throws IOException {
    gzipStream.flush();
  }

  @Override
  public void write(byte b[]) throws IOException {
    write(b, 0, b.length);
  }

  @Override
  public void write(byte b[], int off, int len) throws IOException {
    if (!open.get()) {
      throw new IOException("Stream closed!");
    }
    gzipStream.write(b, off, len);
  }

  @Override
  public void write(int b) throws IOException {
    if (!open.get()) {
      throw new IOException("Stream closed!");
    }
    gzipStream.write(b);
  }

  @Override
  public boolean isReady() {
    return false;
  }

  @Override
  public void setWriteListener(WriteListener writeListener) {

  }
}
@SpringBootApplication
@ServletComponentScan
public class SpringBootDemo1Application {

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

@Data
class Order{
  private int id;
  private String name;
}

@Controller
@RequestMapping(value = "/home")
public class HomeController {
  @RequestMapping(value = "/index1",method = RequestMethod.POST)
  @ResponseBody()
  public Order index1(HttpServletRequest request , @RequestBody Order order) {
    return  order;
  }

  @RequestMapping(value = "/index2",method = RequestMethod.POST)
  @ResponseBody()
  public Order index2(HttpServletRequest request , @RequestBody Order order) {
    return  order;
  }
}

请求头填入如下的字段表示发送的是压缩的信息,也可以接收压缩的信息 Accept-Encoding = gzip Content-Encoding =gzip

测试结果

spring boot 提供的 FilterRegistrationBean 注册过滤器

这种方式特别适合spring boot 或 第三方提供的过滤器,推荐使用

上面的 public class MyFilter implements Filter 过滤器类去除所有的注解

@Configuration
public class ConfigFilter {
   @Bean
   public FilterRegistrationBean<MyFilter> filterRegistrationBean(){
       var  filter=new FilterRegistrationBean<MyFilter>();
       filter.setFilter(new MyFilter());
       filter.setOrder(-1);
       filter.setUrlPatterns(Arrays.asList(new String[]{"/home/index1"}));
       filter.setName("myFilter");
       filter.addInitParameter("encoding","UTF-8");
       // 比较方便的是提供了此方法可以决定是否使用此过滤器
       filter.setEnabled(true);
       return  filter;
   }
}

spring 对过滤器的增强

OncePerRequestFilter

Spring 保证OncePerRequestFilter对于给定的请求只执行一次

@WebFilter(urlPatterns = "/*" ,filterName = "myOnePerRequestFilter",initParams = 
{@WebInitParam(name = "encoding",value = "utf-8")})
public class MyOnePerRequestFilter extends OncePerRequestFilter {
  
  //异步请求,配置异步返回的线程是否使用过滤器
  @Override
  protected boolean shouldNotFilterAsyncDispatch() {
    return super.shouldNotFilterAsyncDispatch();
  }

  //异步请求,配置异步返回的线程是否使用过滤器
  @Override
  protected boolean shouldNotFilterErrorDispatch() {
    return super.shouldNotFilterErrorDispatch();
  }

  /**
   * 可以对特定的请求不使用过滤器
   * @param request
   * @return
   * @throws ServletException
   */
  @Override
  protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    return Boolean.TRUE.equals(request.getAttribute("SHOULD_NOT_FILTER"));
  }

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
   FilterChain filterChain) throws ServletException, IOException {
     //getFilterConfig 获取的配置类可以获取过滤器的参数
     var config = this.getFilterConfig();
     config.getInitParameter("encoding");
     //上下文也可以获取
     config.getServletContext();
     //环境变量
     var environment=  this.getEnvironment();
     filterChain.doFilter(request,response);
  }
}

CharacterEncodingFilter - 这个类是专门来解决body编码(乱码问题的)

Servlet Listener 监听器

注册 listener 和 filter 类似

  1. ServletListenerRegistrationBean
  2. @WebListener

Servlet 监听器是 Servlet 规范中定义的一种特殊类,用于监听 ServletContext、HttpSession 和 ServletRequest 等域对象的创建与销毁事件,以及监听这些域对象中属性发生修改的事件。

Listener监听器,目前没有用到使用场景

监听器类型 说明
javax.servlet.ServletContextListener 监听application作用域的创建和销毁
javax.servlet.ServletContextAttributeListener 监听application作用域的属性状态变化
javax.servlet.ServletRequestListener 监听request作用域的创建和销毁
javax.servlet.ServletRequestAttributeListener 监听request作用域的属性状态变化
javax.servlet.http.HttpSessionListener 监听session作用域的创建和销毁
javax.servlet.http.HttpSessionAttributeListener 监听session作用域的属性状态变化
javax.servlet.http.HttpSessionBindingListener 监听对象与session的绑定和解除
javax.servlet.http.HttpSessionActivationListener

spring boot @Async 异步

@Async 代替自己去写多线程去做异步的语法糖 注意点:

  1. @Async 可以加载类上,那此类内所有的非静态公有方法都是异步执行,加在方法上就只有此方法异步执行
  2. 需要 @EnableAsync 配合去扫描所有加了 @Async 注解的方法,去生成代理
  3. 方法的返回值需要
@Component
@Async
public class TestAsync {
  public CompletableFuture<String> test1() throws InterruptedException {
     System.out.println(Thread.currentThread().getName());
     Thread.sleep(1000);
     return CompletableFuture.completedFuture("test1");
  }

  public CompletableFuture<String> test2() throws InterruptedException {
    System.out.println(Thread.currentThread().getName());
    Thread.sleep(2000);
    return CompletableFuture.completedFuture("test2");
  }

  public CompletableFuture<String> test3() throws InterruptedException {
    System.out.println(Thread.currentThread().getName());
    Thread.sleep(3000);
    return CompletableFuture.completedFuture("test3");
  }
}

同步的等待异步完成,耗时是最长线程的时间

@SpringBootApplication
@EnableAsync
public class SpringBootDemo1Application {
  public static void main(String[] args) throws InterruptedException {
    var context=  SpringApplication.run(SpringBootDemo1Application.class, args);
    var bean=context.getBean(TestAsync.class);
    var r1= bean.test1();
    var r2=bean.test2();
    var r3=bean.test3();

    //等待所有任务完成耗时最多是3秒,而不是 1+2+3=6秒
    //同步阻塞异步
    var result= CompletableFuture.allOf(r1, r2, r3).join();
    System.out.println(r1.get());
    System.out.println(r2.get());
    System.out.println(r3.get());
    System.out.println(r1.get());
    System.out.println(r2.get());
    System.out.println(r3.get());
  }
  /*
  task-1
  task-3
  task-2
  //要等3秒才输出
  test1
  test2
  test3
  */
}

异步获取结果

@ServletComponentScan
@SpringBootApplication
@EnableAsync
public class SpringBootDemo1Application {
  public static void main(String[] args) throws InterruptedException, ExecutionException {
    var context=  SpringApplication.run(SpringBootDemo1Application.class, args);
    var bean=context.getBean(TestAsync.class);
    var r1= bean.test1();
    var r2=bean.test2();
    var r3=bean.test3();

    r1.thenAcceptAsync(p->{
      System.out.println(p);
    });
    r2.thenAcceptAsync(p->{
      System.out.println(p);
    });

    r3.thenAcceptAsync(p->{
      System.out.println(p);
    });
    System.out.println("先执行完");
  }
  /*
  先执行完
  task-1
  task-3
  task-2
  test1
  test2
  test3`
  */
}

spring mvc @ControllerAdvice

@ControllerAdvice,是Spring3.2提供的新注解,它是一个Controller增强器,可对controller中被 @RequestMapping注解的方法加一些逻辑处理。主要作用有一下三种:

  1. @ExceptionHandler:用于全局处理控制器里的异常,进行全局异常处理
  2. @InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中,全局数据预处理。
  3. @ModelAttribute:本来作用是绑定键值对到Model中,此处让全局的@RequestMapping都能获得在此处设置的键值对 ,全局数据绑定。

@ControllerAdvice 注解类

/**
 * describe:
 *
 * @author: Hbai
 * @date: 2022/11/29 15:44
 */
@ControllerAdvice
public class MyControllerAdvice {

  /**
   * 全局异常处理
   * @param exception
   * @return
   */
  @ResponseBody
  @ExceptionHandler(Exception.class)
  public Map errorHandler(Exception exception) {
    var map = new HashMap<String, Object>();
    map.put("code", 500);
    var msg = exception.getMessage();
    map.put("msg", msg);
    return map;
  }

  /**
   * 绑定数据到全局数据 Model 中,在所有 control 中都可以获取到
   * @return
   */
  @ModelAttribute(name = "ma")
  public Map getGlobalData1() {
    var map = new HashMap<String, Object>();
    map.put("id", 1);
    map.put("name", "jim");
    return map;
  }

  @ModelAttribute(name = "mb")
  public Map getGlobalData2() {
    var map = new HashMap<String, Object>();
    map.put("id", 2);
    map.put("name", "bob");
    return map;
  }

  /**
   * 对 web 传入的参数进行预处理
   * @param binder
   */
  @InitBinder("a")
  public void webBinder1(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("x.");
  }

  @InitBinder("b")
  public void webBinder2(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("y.");
  }
}

测试类

@Controller
@RequestMapping(value = "/home")
public class HomeController {
  @RequestMapping(value = "/index1",method = RequestMethod.GET)
  @ResponseBody()
  public Object index1(Model model) {
    var  map = model.getAttribute("ma");
    if(map instanceof HashMap){
       var temp = (HashMap<String,Object>)map;
       return  temp;
    }
    return null;
  }
  @RequestMapping(value = "/index2",method = RequestMethod.GET)
  @ResponseBody()
  public Object index2(Model model) {
    var  map = model.getAttribute("mb");
    if(map instanceof HashMap){
      var temp = (HashMap<String,Object>)map;
      return  temp;
    }
    return null;
  }


  @Data
  class A{
    private int a;
    private String name;
  }

  @Data
  class B{
    private int b;
    private String name;
  }
  
  /*
   传入的参数要和 InitBinder 指定的前缀进行匹配
   Params 参数如下:
     a=1
     b=2
     x.name=jim
     y.name=jim
  */
  @RequestMapping(value = "/index3",method = RequestMethod.GET)
  @ResponseBody()

  public Object index3(@ModelAttribute("a") A a,@ModelAttribute("b") B b) {
      return  a.toString()+","+b.toString();
  }

}
👍🎉🎊