大纲:
- 掌握拦截器的使⽤,及其原理
- 学习统⼀数据返回格式和统⼀异常处理的操作
- 了解⼀些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就⾏了
/**
* 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);
}
}
//客户端调用
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注解
@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类型时,不需要进行处理
下面测试多组类型:
@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 是异常处理器,两个结合表⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件
@ResponseBody
@ControllerAdvice
@Slf4j
public class ErrorHandler {
@ExceptionHandler
public Result exception(Exception e) {
log.error("发生异常,e:{}", e);
return Result.fail2("内部错误");
}
}
以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类),就返回⼀个Result的对象
针对不同的异常,返回不同的结果:
@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异常");
}
}
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配