SpringBoot 统⼀功能处理

大纲:

  • 掌握拦截器的使⽤,及其原理
  • 学习统⼀数据返回格式和统⼀异常处理的操作
  • 了解⼀些Spring的源码

1. 拦截器

之前写登录程序时,后端程序根据Session来判断⽤⼾是否登录,但是实现⽅法是⽐较⿇烦的,需要修改很多代码,这里引入拦截器简化

1.1 什么是拦截器

拦截器是Spring框架提供的核⼼功能之⼀,主要⽤来拦截⽤⼾的请求,在指定⽅法前后,根据业务需要执⾏预先设定的代码.

(也就是说,允许开发⼈员提前预定义⼀些逻辑,在⽤⼾的请求响应前后执⾏.也可以在⽤⼾请求前阻⽌其执⾏)

举个例子:

⽐如我们去银⾏办理业务,在办理业务前后,就可以加⼀些拦截操作

办理业务之前,先取号,如果带⾝份证了就取号成功

这些就是"拦截器"做的⼯作

1.2 拦截器使用

1.2.1 定义拦截器

实现HandlerInterceptor接⼝,并重写其所有⽅法

  • preHandle()⽅法:⽬标⽅法执⾏前执⾏. 返回true:继续执⾏后续操作;返回false:中断后续操作.
  • postHandle()⽅法:⽬标⽅法执⾏后执⾏
  • afterCompletion()⽅法:视图渲染完毕后执⾏,最后执⾏(后端开发现在⼏乎不涉及视图,暂不了 解)

1.2.2 注册配置拦截器

实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法

preHandle⽅法返回true

如果没有登录:

登录后:


返回false:

拦截器拦截了请求,没有进行响应


|------------|--------------|---------------------------------------------------------|
| 拦截路径 | 含义 | 举例 |
| /* | ⼀级路径 | 能匹配/user,/book,/login,不能匹配/user/login |
| /** | 任意级路径 | 能匹配/user,/user/login,/user/reg |
| /book/* | /book下的⼀级路径 | 能匹配/book/addBook,不能匹 配/book/addBook/1,/book |
| /book/** | /book下的任意级路径 | 能匹配/book,/book/addBook,/book/addBook/2,不 能匹配/user/login |

注:

  • 以上拦截规则可以拦截此项⽬中的使⽤URL,包括静态⽂件(图⽚⽂件,JS和CSS等⽂件)
  • 添加拦截器后,执⾏Controller的⽅法之前,请求会先被拦截器拦截住.执⾏ preHandle() ⽅法,这个⽅法需要返回⼀个布尔类型的值.如果返回true,就表⽰放⾏本次操作,继续访问controller中的⽅法.如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).
  • controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及 afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据.

2. 适配器模式

2.1 定义

适配器模式,也叫包装器模式.将⼀个类的接⼝,转换成客⼾期望的另⼀个接⼝,适配器让原本接⼝不兼容的类可以合作⽆间.

简单来说就是⽬标类不能直接使⽤,通过⼀个新类进⾏包装⼀下,适配调⽤⽅使⽤.把两个不兼容的接口通过⼀定的⽅式使之兼容.

比如:

2.2 角色

  • Target:⽬标接⼝(可以是抽象类或接⼝),客⼾希望直接⽤的接⼝
  • Adaptee:适配者,但是与Target不兼容
  • Adapter:适配器类,此模式的核⼼.通过继承或者引⽤适配者的对象,把适配者转为⽬标接⼝
  • client: 需要使⽤适配器的对象

2.3 实现

slf4j就使⽤了适配器模式,slf4j提供了⼀系列打印⽇志的api,底层调⽤的是log4j或者 logback来打⽇志,我们作为调⽤者,只需要调⽤slf4j的api就⾏了

java 复制代码
/**
 * slf4j接口
 */
public interface Slf4jApi {
    void log(String message);
}


/**
 * log4j
 */
public class Log4j {

    public void log(String message){
        System.out.println("Log4j打印日志:"+message);
    }
}


/**
 * 适配器
 */
public class Slf4jLog4jAdapter implements Slf4jApi{

    private Log4j log4j;

    public Slf4jLog4jAdapter(Log4j log4j) {
        this.log4j = log4j;
    }

    @Override
    public void log(String message) {
        log4j.log(message);
    }
}
java 复制代码
//客户端调用
public class Main {
    public static void main(String[] args) {
        Slf4jApi api = new Slf4jLog4jAdapter(new Log4j());
        api.log("我是通过slf4j打印的日志");
    }
    
}

可以看出,我们不需要改变log4j的api,只需要通过适配器转换下,就可以更换⽇志框架,保障系统的平稳 运⾏.

2.4 适用场景

⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷.应⽤这种模式算是"⽆奈之 举", 如果在设计初期,我们就能协调规避接⼝不兼容的问题,就不需要使⽤适配器模式了

所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造,并且希望可以复⽤原有代码实现新的功能.⽐如版本升级等

3. 统一数据返回格式

强制登录案例中,我们共做了两部分⼯作:

  • 通过Session判断用户是否登录
  • 对后端返回数据进⾏封装,告知前端处理的结果

拦截器帮我们实现了第⼀个功能,接下来看SpringBoot对第⼆个功能如何⽀持

2.1 引入

统⼀的数据返回格式使⽤ @ControllerAdvice 和 ResponseBodyAdvice 的⽅式实现,@ControllerAdvice 表⽰控制器通知类

添加类@ResponseAdvice,实现ResponseBodyAdvice接口,并在类上添加@ControllerAdvice注解

java 复制代码
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {


    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

   
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return Result.success(body);
    }
}
  • supports⽅法: 判断是否要执⾏beforeBodyWrite⽅法.true为执⾏,false不执⾏. 通过该⽅法可以 选择哪些类或哪些⽅法的response要进⾏处理,其他的不进⾏处理.
  • beforeBodyWrite⽅法: 对response⽅法进⾏具体操作处理

2.2 存在问题

对上述代码进行测试后发现,返回结果为String类型时,不能正确进行处理

返回结果为Result类型时,不需要进行处理

下面测试多组类型:

java 复制代码
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public Boolean t1(){
        int a = 10/0;
        return true;
    }


    @RequestMapping("/t2")
    public Integer t2(){
        String a = null;
        System.out.println(a.length());
        return 123;
    }


    @RequestMapping("/t3")
    public String t3(){
        int[] a = {1,2,3,4};
        System.out.println(a[4]);
        return "hello";
    }


    @RequestMapping("/t4")
    public BookInfo t4(){
        return new BookInfo();
    }

    @RequestMapping("/t5")
    public Result t5(){
        return Result.success("success");
    }

}

测试后, 发现只有返回结果为String类型时才有错误发⽣.


解决:

①String

如果返回结果为 String 类型 , 使⽤ SpringBoot 内置提供的 Jackson 来实现信息的序列化

@SneakyThrows也是lombok提供的

如果后端返回的结果时String类型,当我们使用统一结果返回时,返回的是JSON字符串,

content-type是text/html

我们需要转为JSON,后端转换:

②result

如果⼀些⽅法返回的结果已经是Result类型了,那就直接返回Result类型的结果即可

2.3 优点

  • ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据
  • 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接⼝都是这样返回的
  • 有利于项⽬统⼀数据的维护和修改
  • 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容

4. 统一异常处理

统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的

@ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件

java 复制代码
@ResponseBody
@ControllerAdvice
@Slf4j
public class ErrorHandler {

    @ExceptionHandler
    public Result exception(Exception e) {
        log.error("发生异常,e:{}", e);
        return Result.fail2("内部错误");
    }

}

以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类),就返回⼀个Result的对象

针对不同的异常,返回不同的结果:

java 复制代码
@ResponseBody
@ControllerAdvice
@Slf4j
public class ErrorHandler {

    @ExceptionHandler
    public Result exception(Exception e){
      log.error("发生异常,e:{}",e);
      return Result.fail2("内部错误");
    }


    @ExceptionHandler
    public Result exception(ArithmeticException e){
        log.error("发生异常,e:{}",e);
        return Result.fail2("ArithmeticException异常");

    }

    @ExceptionHandler
    public Result exception(NullPointerException e){
        log.error("发生异常,e:{}",e);
        return Result.fail2("NullPointerException异常");

    }
}

当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配

相关推荐
WaaTong16 分钟前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_7430484416 分钟前
初识Java EE和Spring Boot
java·java-ee
AskHarries18 分钟前
Java字节码增强库ByteBuddy
java·后端
佳佳_32 分钟前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
小灰灰__38 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭41 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果1 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林1 小时前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el
许野平2 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono