目录
1. 用户登录权限校验
1.1 最初用户登录权限效验
1.2 Spring AOP 用户统⼀登录验证
1.3 Spring 拦截器
(1)创建自定义拦截器
(2)将自定义拦截器添加到系统配置中,并设置拦截的规则
1.4 练习:登录拦截器
(1)实现 UserController 实体类
(2)返回的登录页面:login.html
(3)实现效果
1.5 拦截器实现原理
(1)实现原理源码分析
1.6 统一访问前缀添加
(1)在系统的配置文件中设置
(2)在 application.properies 中配置
2. 统一的异常处理
2.1 异常的统一封装
(1)创建一个类,并在类上标识:@ControllerAdvice
(2)添加方法 @ExceptionHandler 来订阅异常
3. 统一数据返回格式
3.1 为什么要统一数据返回格式
3.2 统一数据返回格式的实现
(1)创建一个类,并添加 @ControllerAdvice
(2)实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyAdvice 方法
4. @ControllerAdvice 源码分析
(1) @ControllerAdvice 源码
(2)查看 initializingBean 有哪些实现类
(3)查询 initControllerAdviceCache 方法
本节主要讲解Spring Boot 统一功能处理,同样也是 AOP 的实战环节,我们希望能够实现以下目标:
- 统一用户登陆权限验证
- 统一异常处理
- 统一数据格式返回
1. 用户登录权限校验
回顾一下最初用户登录验证的实现方法:
- 最初的用户登录校验版本:在每个方法中获取 Session 以及 Session 中的信息,对用户账号以及密码进行校验,正确则登录成功,反之则失败
- 第二版本:实现统一方法去校验是否登陆成功,在每个需要验证的方法中调用统一的用户登录身份效验方法来判断
- 第三版本:使用 Spring AOP 来进行用户统一登录校验
- 第四版本:使用 Spring 拦截器来实现用户的统一登录验证
1.1 最初用户登录权限效验
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/a1")
public Boolean login (HttpServletRequest request) {
// 有 Session 就获取,没有就不创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录,进行业务处理
return true;
} else {
// 未登录
return false;
}
}
@RequestMapping("/a2")
public Boolean login2 (HttpServletRequest request) {
// 有 Session 就获取,没有就不创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录,进行业务处理
return true;
} else {
// 未登录
return false;
}
}
}
这种方式写的代码,每个方法中都有相同的用户登录验证权限,缺点是:
- 每个方法中都要单独写用户登录验证的方法,即使封装成公共方法,也一样要传参调用和在方法中进行判断
- 添加控制器越多,调用用户登录验证的方法也越多,这样就增加了后期的修改成功和维护成功
- 这些用户登录验证的方法和现在要实现的业务几乎没有任何关联,但还是要在每个方法中都要写一遍,所以提供一个公共的 AOP 方法来进行统一的用户登录权限验证是非常好的解决办法。
1.2 Spring AOP 用户统⼀登录验证
统一用户登录验证,首先想到的实现方法是使用 Spring AOP 前置通知或环绕通知来实现:
@Aspect // 当前类是一个切面
@Component
public class UserAspect {
// 定义切点方法 Controller 包下、子孙包下所有类的所有方法
@Pointcut("execution(* com.example.springaop.controller..*.*(..))")
public void pointcut(){}
// 前置通知
@Before("pointcut()")
public void doBefore() {}
// 环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
System.out.println("Around 方法开始执行");
try {
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("Around 方法结束执行");
return obj;
}
}
但如果只在以上代码 Spring AOP 的切面中实现用户登录权限效验的功能,有这样两个问题:
- 没有办法得到 HttpSession 和 Request 对象
- 我们要对一部分方法进行拦截,而另一部分方法不拦截,比如注册方法和登录方法是不拦截的,也就是实际的拦截规则很复杂,使用简单的 aspectJ 表达式无法满足拦截的需求
1.3 Spring 拦截器
针对上面代码 Spring AOP 的问题,Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现有两步:
- 创建自定义拦截器,实现 Spring 中的 HandlerInterceptor 接口中的 preHandle方法
- 将自定义拦截器加入到框架的配置中,并且设置拦截规则
(1)创建自定义拦截器
//实现 HandlerInterceptor 接口
public class loginInterceptor implements HandlerInterceptor {
/**
* 返回 true 继续下序流程
* false 表示验证失败
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 用户登录业务判断
// false 表示当不存在 session 不存在时不需要创造一个会话信息
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null){
// 说明用户已经登录
return true;
}
// 可以直接跳转到登录页面 或 返回一个 401、403 没有权限码
response.sendRedirect("/login.html");
// response.setStatus(401);
return false;
}
}
(2)将自定义拦截器添加到系统配置中,并设置拦截的规则
- addPathPatterns:表示需要拦截的 URL,**表示拦截所有⽅法
- excludePathPatterns:表示需要排除的 URL
@Configuration // 让随着spring启动而启动
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new loginInterceptor())
.addPathPatterns("/**")// 拦截所有请求
.excludePathPatterns("/user/login")// 不拦截的 url 地址
.excludePathPatterns("/user/reg")
.excludePathPatterns("/**/*.html");
}
}
1.4 练习:登录拦截器
实现愿望:
- 登录、注册页面不拦截,其余页面都拦截
- 等登陆成功写入 session 后,拦截页面可访问
(1)实现 UserController 实体类
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getUser")
public String getuser(){
System.out.println("执行了 getUser !");
return "get user";
}
@RequestMapping("/login")
public String login(){
System.out.println("执行了 login !");
return "get login";
}
@RequestMapping("/reg")
public String reg(){
System.out.println("执行了 reg !");
return "get reg";
}
}
(2)返回的登录页面:login.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登录页面</title>
</head>
<body>
<h1>登录页面</h1>
</body>
</html>
(3)实现效果
1.5 拦截器实现原理
(1)实现原理源码分析
- 所有的 Controller 执行都会通过一个调度器 DispatcherServlet 来实现
- 而所有方法都会执行 DispatcherServlet 中的 doDispatch 调度⽅法,doDispatch 源码分析如下:
通过源码分析,可以看出,Sping 中的拦截器也是通过动态代理和环绕通知的思想实现的
1.6 统一访问前缀添加
方法:
- 在系统的配置文件中设置
- 在 application.properies 中配置
(1)在系统的配置文件中设置
/**
* 所有的接口添加 api 前缀
* c 代表所有的请求(Controller)
* 表示所有的地址都会加上这个前缀
* @param configurer
*/
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api",c -> true);
}
现在我们去查看之前不被拦截的地址
(2)在 application.properies 中配置
2. 统一的异常处理
- 给当前的类上加 @ControllerAdvice 表示控制器通知类
- 给方法上添加 @ExceptionHandler(xxx.class),表示异常处理器,添加异常返回的业务代码
我们先去制造些异常:
2.1 异常的统一封装
(1)创建一个类,并在类上标识:@ControllerAdvice
@ControllerAdvice
public class ExceptionHandler {
}
(2)添加方法 @ExceptionHandler 来订阅异常
@ControllerAdvice
@ResponseBody// 表示当前的所有方法返回的都是数据不是页面
public class ExHandler {
/**
* 拦截所有的空指针异常,继续统一的数据返回
*/
@ExceptionHandler(NullPointerException.class)// 空指针异常
public HashMap<String,Object> nullException(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code","-1");
result.put("msg","空指针异常:" + e.getMessage());//错误码的描述信息
result.put("date",null);
return result;
}
}
但是需要考虑的一点是,如果每个异常都这样写,那么工作量是非常大的,并且还有自定义异常,所以上面这样写肯定是不好的,既然是异常直接写 Exception 就好了,它是所有异常的父类,如果遇到不是前面写的两种异常,那么就会直接匹配到 Exception
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配
@ControllerAdvice
@ResponseBody// 表示当前的所有方法返回的都是数据不是页面
public class ExHandler {
/**
* 拦截所有的空指针异常,继续统一的数据返回
*/
@ExceptionHandler(NullPointerException.class)// 空指针异常
public HashMap<String,Object> nullException(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code","-1");
result.put("msg","空指针异常:" + e.getMessage());//错误码的描述信息
result.put("date",null);
return result;
}
@ExceptionHandler(Exception.class)// 所有异常
public HashMap<String,Object> AllException(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code","-1");
result.put("msg","异常:" + e.getMessage());//错误码的描述信息
result.put("date",null);
return result;
}
}
3. 统一数据返回格式
3.1 为什么要统一数据返回格式
- 方便前端程序员更好的接收和解析后端数据接口返回的数据。
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就行了,因为所有接口都是这样返回的
- 有利于项目统一数据的维护和修改。
- 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容。
3.2 统一数据返回格式的实现
(1)创建一个类,并添加 @ControllerAdvice
@ControllerAdvice
public class ResponseAdvice {
}
(2)实现 ResponseBodyAdvice 接口,并重写 supports 和 beforeBodyAdvice 方法
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* 表示是否需要重写
* 返回true则执行beforeBodyWrite方法,反之则不执行
*/
@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) {
HashMap<String,Object> hashMap = new HashMap<>();
hashMap.put("code",200);// 状态码
hashMap.put("msg","");// 错误的描述信息
hashMap.put("date",body);
return hashMap;
}
}
supports方法相当于是一个开关,只有当 true 时才能执行重写 beforeBodyWrite 方法,false就不重写
当访问 getUser 时发生异常了,类型访问异常
注意:
我们知道String既不属于基本数据类型,又不属于对象,且在重写方法的时候其余类型都是用的统一的格式化工具,而String用的是它自身的格式化工具,String自身的格式化工具在执行的时候还没有加载好,就会导致 原始类型 是String的时候,在转化成HashMap的时候就会报错
所以在统一返回的时候需要对String进行单独的处理
jackson就是用于 json 数据转换的,json的转换工具
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String,Object> hashMap = new HashMap<>();
hashMap.put("code",200);// 状态码
hashMap.put("msg","");// 错误的描述信息
hashMap.put("date",body);
if (body instanceof String){
// 判断数据类型是不是 String,是String需要特殊处理,因为 String 在转换的时候会报错
try {
return objectMapper.writeValueAsString(hashMap);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return hashMap;
}
4. @ControllerAdvice 源码分析
通过对 @ControllerAdvice 源码的分析我们可以知道上面统一异常和统一数据返回的执行流程
(1) @ControllerAdvice 源码
可以看到 @ControllerAdvice 派生于 @Component 组件而所有组件初始化都会调用 InitializingBean 接口
(2)查看 initializingBean 有哪些实现类
在查询过程中发现,其中 Spring MVC 中的实现子类是 RequestMappingHandlerAdapter,它里面有一个方法 afterPropertiesSet()方法,表示所有的参数设置完成之后执行的方法
(3)查询 initControllerAdviceCache 方法
发现这个方法在执行时会查找使用所有的 @ControllerAdvice 类,发送某个事件时,调用相应的 Advice 方法,比如返回数据前调用统一数据封装,比如发生异常是调用异常的 Advice 方法实现的