目录
[1 拦截器](#1 拦截器)
[1.1 什么是拦截器](#1.1 什么是拦截器)
[1.2 拦截器的使用](#1.2 拦截器的使用)
[1.2.1 定义拦截器](#1.2.1 定义拦截器)
[1.2.2 注册配置拦截器](#1.2.2 注册配置拦截器)
[1.3 拦截器详情](#1.3 拦截器详情)
[1.3.1 拦截路径](#1.3.1 拦截路径)
[1.3.2 拦截器执行流程](#1.3.2 拦截器执行流程)
[1.4 登录校验](#1.4 登录校验)
[1.4.1 定义拦截器](#1.4.1 定义拦截器)
[1.4.2 注册配置拦截器](#1.4.2 注册配置拦截器)
[2 适配器模式](#2 适配器模式)
[3 统一数据返回](#3 统一数据返回)
[3.1 存在问题](#3.1 存在问题)
[3.2 优点](#3.2 优点)
[4 统一异常处理](#4 统一异常处理)
1 拦截器
1.1 什么是拦截器
拦截器是Spring框架提供的核心功能之一,主要用来拦截用户的请求,在指定方法前后,根据业务需要执行预先设定的代码,例如在一个项目中,不管进入哪个模块,都需要强制登录,此时拦截器的作用就发挥出来了
1.2 拦截器的使用
拦截器的使用步骤分为两步:
1)定义拦截器 2)注册配置拦截器
1.2.1 定义拦截器
实现HandlerInterceptor接口,并重写其所有方法
java
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
log.info("LoginInterceptor 目标方法执行前执行");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
log.info("LoginInterceptor 目标方法执行后执行");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
log.info("LoginInterceptor 视图渲染完毕后执⾏,最后执⾏");
}
}
preHandle()方法:目标方法执行前执行,返回true,表示放行,返回false,表示拦截
postHandle()方法:目标方法执行后执行
afterCompletion()方法:视图渲染完毕之后执行,最后执行
1.2.2 注册配置拦截器
实现WebMvcConfigurer接口,并重写addInterceptors方法
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截对象
@Autowired
private LoginInterceptor loginInterceptor;
private List<String> excludePaths = Arrays.asList(
"/user/login",
"/css/**",
"/js/**",
"/pic/**",
"/**/*.html"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") //给所有请求设置拦截
.excludePathPatterns(excludePaths); //排除登录的拦截
}
}
启动服务器,访问任意请求,观察后端日志
可以看到perHandle方法执行之后就放行了,开始执行目标方法,目标方法执行完之后执行postHandle方法
1.3 拦截器详情
1.3.1 拦截路径
拦截路径是指我们定义的这个拦截器,对哪些请求生效,在注册配置拦截器的时候,通过addPathPatterns()方法指定要拦截哪些请求,也可以通过excludePathPatterns()指定放行哪些请求,在上述代码中,除了登录和前端的代码,对其他所有的代码进行了拦截
|------------|--------------|---------------------------------------------------------|
| 拦截路径 | 含义 | 举例 |
| /* | 一级路径 | 能匹配/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 |
1.3.2 拦截器执行流程
正常的调用顺序是,Controller层调用Service层,Service层调用数据持久层这样的三层架构,有了拦截器之后,会在调用Controller层之前进行响应的业务处理,执行流程如下图
1 添加拦截器之后,在执行Controller层的方法之前,请求会先被拦截,执行preHandle()方法,如果这个方法返回true,就表示放行,继续访问Controller层中的方法,也就是执行三层架构,Controller------Service------Mapper,如果返回false,表示拦截,后续Controller层的方法不会执行
2 Controller当中的方法执行完毕之后,在回过来执行postHandle方法,执行完毕之后,最终给浏览器响应数据
1.4 登录校验
通过拦截器来完成登录的校验功能
1.4.1 定义拦截器
java
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("登录拦截器校验");
HttpSession session = request.getSession();
UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
if (userInfo != null && userInfo.getId() > 0) {
//表示放行
return true;
}
response.setStatus(401);
//表示拦截
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("目标方法执行后");
}
}
1.4.2 注册配置拦截器
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截对象
@Autowired
private LoginInterceptor loginInterceptor;
private List<String> excludePaths = Arrays.asList(
"/user/login",
"/css/**",
"/js/**",
"/pic/**",
"/**/*.html"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") //给所有请求设置拦截
.excludePathPatterns(excludePaths); //排除登录的拦截
}
}
使用postman来进行测试
没有登陆时,访问任意一个模块,此时会被拦截
登录之后访问,此时就会放行
2 适配器模式
适配器模式,也叫包装器模式,将一个类的接口转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以合作,简单来说就是将一个新类进行包装一下,适配调用使用,把两个不兼容的接口通过一定的方式使之兼容
适配器模式角色
Target:目标接口(可以是抽象类或者接口),客户希望直接使用的接口
Adaptee:适配者,但是与Target不兼容
Adapter:适配器类,此模式的核心,通过继承或者引用适配者的对象,把适配者转为目标接口
client:需要使用的适配器对象
适配器模式的实现
**场景:**SLF4J就是用了适配器模式,SLF4J提供了一系列打印日志的api,底层调用的是log4j或者logback来打印日志,我们作为为调用者,只需要调用slf4j的api就可以了
java
/**
* slf4j接口
*/
interface Slf4jApi {
void log(String mes);
}
/**
* log4j接口
*/
class Log4j {
void log4jLog(String mes) {
System.out.println("log4jLog打印:" + mes);
}
}
/**
* slf4j和log4j适配器
*/
class Slf4jLog4JAdapter implements Slf4jApi{
private Log4j log4j;
public Slf4jLog4JAdapter(Log4j log4j) {
this.log4j = log4j;
}
@Override
public void log(String mes) {
log4j.log4jLog(mes);
}
}
/**
* 客户端使用
*/
public class Slf4jDemo {
public static void main(String[] args) {
Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j());
slf4jApi.log("使用slf4j打印日志");
}
}
上述代码中。不需要改变log4j的api,只需要通过适配器转换下,就可以更换日志框架,保障系统的平稳运行
3 统一数据返回
统一数据返回格式使用@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);
}
}
java
@Data
public class Result {
/**
* 业务状态码
*/
private ResultCode code;
/**
* 错误信息
*/
private String errMsg;
/**
* 数据
*/
private Object data;
public static Result success(Object data) {
Result result = new Result();
result.setCode(ResultCode.SUCCESS);
result.setErrMsg("");
result.setData(data);
return result;
}
public static Result fail(String errMsg) {
Result result = new Result();
result.setCode(ResultCode.FAIL);
result.setErrMsg(errMsg);
result.setData("");
return result;
}
public static Result unLogin() {
Result result = new Result();
result.setCode(ResultCode.UNLOGIN);
result.setErrMsg("用户未登录");
result.setData(null);
return result;
}
}
以上的Result类是我在图书项目中定义的,我会在另一篇博客完整的写整个图书后端的代码
support方法:判断是否执行beforeBodyWrite方法,true为执行,false为不执行,通过该方法可以选择哪些类或者哪些方法的response要进行处理
beforeBodyWrite方法:对response方法进行具体操作处理
添加统一数据返回格式之前
添加统一数据返回格式之后
3.1 存在问题
如果返回的类型为String类型时,会发生内部错误,但是在数据库中,这条数据修改成功了
查看日志,会发现日志不能捕获到String类型
解决方案:
java
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper mapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
//使用这个注解来捕获异常
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//在返回之前,如果body是返回结果直接返回
if (body instanceof Result) {
return body;
}
//如果返回的是String类型,需要使用SpringBoot内置的Jackson来实现信息的序列化
if (body instanceof String) {
return mapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
此时的结果可以正确的返回了
3.2 优点
1 方便前端更好的接收和解析后端数据接口返回的数据
2 降低前后端的沟通成本,按照某个格式实现就可以
3 有利于项目统一数据的维护和修改
4 统一异常处理
统一异常处理使用的是@ControllerAdvice + @ExceptionHandler来实现的,@ControllerAdvice表示控制器通知类,@ExceptionHandler是异常处理器,两个结合表示出现异常的时候执行某个通知,也就是执行某个方法的事件
java
@Slf4j
@ControllerAdvice
@ResponseBody
public class ErrorHandler {
@ExceptionHandler
public Object exception(Exception e) {
log.error("发生异常,e:" + e);
return Result.fail(e.getMessage());
}
}
如果上述代码出现 Exception异常(包括Exception的子类),就会返回一个Result对象
java
public static Result fail(String errMsg) {
Result result = new Result();
result.setCode(ResultCode.FAIL);
result.setErrMsg(errMsg);
result.setData("");
return result;
}
上述代码需要注意:
1)接口返回为数据时,需要加上@ResponseBody注解
2)@ControllerAdvice + @ExceptionHandler两个注解不能忘记,必须加上
可以针对不同的异常,来返回不同的结果
java
@Slf4j
@ControllerAdvice
@ResponseBody
public class ErrorHandler {
@ExceptionHandler
public Object exception(Exception e) {
log.error("发生异常,e:" + e);
return Result.fail("发生异常" + e.getMessage());
}
@ExceptionHandler
public Object exception(NullPointerException e) {
log.error("发生异常,e:" + e);
return Result.fail("发生NullPointerException:" + e.getMessage());
}
@ExceptionHandler
public Object exception(ArithmeticException e) {
log.error("发生异常,e:" + e);
return Result.fail("发生ArithmeticException:" + e.getMessage());
}
}
下面制造一些异常,依次测试其抛出的异常
java
@RestController
@RequestMapping("/test")
public class TextController {
@RequestMapping("/t1")
public String t1(){
return "t1";
}
@RequestMapping("/t2")
public boolean t2(){
int a = 10/0;
return true;
}
@RequestMapping("/t3")
public Integer t3() {
String a = null;
System.out.println(a.length());
return 200;
}
}
可以看出t2抛出算数异常,t3抛出空指针异常 ,虽然ArithmeticException和NullPointerException都继承Exception,但是有多个异常的时候,匹配顺序为当前类及其子类向上依次匹配,会找到离它最近的那个异常