Spring AOP
1.什么是AOP
AOP(Aspect Oriented Programming):面向切面编程,它是⼀种思想,它是对某⼀类事情的集中处理。 AOP 是⼀种思想,而 Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和IoC 与 DI 类似。
比如
没学AOP之前,有一个项目,这个项目有8个页面,这个项目的每一个页面进去之前都需要判断用户是否登录
验证是否登录就可以归类为一类事情
学了AOP之后,就可以在AOP这个框架设置一下,把这个‘一类事情’通过AOP框架设置一下,就不需要每个页面进去之后都需要验证用户是否登录了,这样就是判断直接在AOP框架实现了,如果没登陆直接就进不了这几个页面,降低了耦合性,因为判断是一件事,进入页面也是一件事,为啥又要进页面又要判断呢,这不是复杂化了嘛
或者还是不懂的话,换一种方式理解
一个火车站有10个站台,假设安装一个小安保设备是10万,大安保设备是50万
不集中处理就是这10个站台每个站台都安装一个安保设备,花了100万,10个设备都需要保安检查,就需要花10队保安的钱,然后这时候有一队保安需要调走,于是就有一个车站空了,这时候就需要从另外9队保安调人过来,这就牵一发动全身了
集中处理就是在火车站进站口花50万安装一个大的安保设备,只需要花一队保安的钱,这时候这队保安需要走,可以再调一队过来,就可以把保安留还是调走这件事集中处理了
2.为什么要用AOP
想象⼀个场景,我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几乎所有页面调用的前端控制器( Controller)都需要先验证用户登录的状态,那这个时候我们要怎么处理呢?
我们之前的处理方式是每个 Controller 都要写⼀遍用户登录验证,然而当你的功能越来越多,那么你要写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会代码修改和维护的成本。那有没有简单的处理方案呢?答案是有的,对于这种功能统⼀,且使用的地方较多的功能,就可以考虑 AOP来统⼀处理了
除了统⼀的用户登录判断之外,AOP 还可以实现:
统⼀日志记录
统⼀方法执行时间统计
统⼀的返回格式设置
统⼀的异常处理
事务的开启和提交等
也就是说使用AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。
3.AOP组成
3.1切面(Aspect)
定义的是事件,也就是AOP针对哪一方面的(是用来检测登录的还是用来输出日志的)
切面相当于老板:定义公司的方向
3.2切点(Pointcut)
定义具体规则的。
比如用户登录,哪些接口需要进行用户登录,哪些不需要进行,这个就是个规则,需要让AOP按照这个规则进行
切点相当于公司中层:指定具体的方案
3.3通知(Advice)
AOP执行的具体方法
比如获取用户登录信息(session),获取到就是登录,接着往下进行,没获取到返回false
通知相当于公司的底层:具体业务执行者
通知又分为5个:
1.前置通知使用 @Before:通知方法会在目标方法调用之前执行。
2.后置通知使用 @After:通知方法会在目标方法返回或者抛出异常后调用。
3.返回之后通知使用 @AfterReturning:通知方法会在目标方法返回后调用。
4.抛异常后通知使用 @AfterThrowing:通知方法会在目标方法抛出异常后调用。
5.环绕通知使用 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
3.4连接点(JoinPoint)
有可能触发切点的点
比如用户登录这个例子,连接点就是每个接口,因为每个接口都可能触发用户验证登录这个条件
4.Spring AOP实现原理
Spring AOP 是构建在动态代理基础上
这俩的区别:
1.JDK 动态代理基于接口,要求目标对象实现接口;CGLIB 动态代理基于类,可以代理没有实现接口的目标对象。
2.JDK 动态代理使用 java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler 来生成代理对象;CGLIB 动态代理使用 CGLIB 库来生成代理对象。(出身不同,JDK 动态代理来源java,CGLIB来源于第三方)
3.JDK 动态代理生成的代理对象是目标对象的接口实现;CGLIB 动态代理生成的代理对象是目标对象的子类。
4.JDK 动态代理性能相对较高,生成代理对象速度较快;CGLIB 动态代理性能相对较低,生成代理对象速度较慢。JDK7之前CGLIB 性能高于JDK 动态代理,7之后就反过来了
5.CGLIB 动态代理无法代理 final 类和 final 方法;JDK 动态代理可以代理任意类。
5.AOP实战
5.1用户登录权限校验
5.1拦截器
之前的AOP的步骤太繁琐了,并且获取不到session对象,为此Spring提供了拦截器HandlerInterceptor,拦截器的实现分为以下两个步骤
1.自定义拦截器
UserInterceptor代码
//自定义拦截器
//HandlerInterceptor可以看成用来管理所有拦截器的管理器
//同时用注解将UserInterceptor注入到ioc容器中
@Component
public class UserInterceptor implements HandlerInterceptor
{
//返回true,表明拦截器验证成功,继续执行后面的方法
//返回false,表明拦截器验证失败,不会执行后面的方法
//preHandle可以看成是执行目标方法之前的方法,因为拦截器就是在执行目标方法之前判断是否登录的
//以下方法在idea自动生成步骤:右键->generate->override method
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
//先获取session对象,没获取到就不创建新的session对象了
HttpSession session= request.getSession(false);
if(session!=null&&session.getAttribute(AppVar.SESSION_KEY)!=null)
return true;
return false;
}
}
AppVar代码
//这个方法是定义session的值的
public class AppVar
{
//Session的值
public static final String SESSION_KEY="SESSION_KEY";
}
2.将自定义拦截器配置到系统设置中,并自定义规则
//这个类继承了WebMvcConfigurer之后就是一个系统的配置文件
//且必须加上@Configuration注解
@Configuration
public class AppConfig implements WebMvcConfigurer
{
@Autowired
private UserInterceptor userInterceptor;
//addInterceptors是添加多个拦截器,在一个项目中,拦截器可以有多个
@Override
public void addInterceptors(InterceptorRegistry registry)
{
//获取到拦截器
registry.addInterceptor(userInterceptor)
//设置拦截规则
.addPathPatterns("/**")//拦截所有请求
.excludePathPatterns("/user/reg")//不拦截(放行)注册请求
.excludePathPatterns("/user/login");//不拦截(放行)登录请求
//如果根据需求还可以放行更多的,就还继续.excludePathPatterns就行了
;
}
}
拦截器已经写好了,下面来一个测试类来测试我们自己写的拦截器
@RestController
@RequestMapping("/user")
public class UserController
{
//这个方法没说放行,所以访问的时候会被拦截的
@RequestMapping("/getuser")
public String getUser()
{
System.out.println("执行了getUser方法");
return "user";
}
//这个没被拦截,可以执行
@RequestMapping("/reg")
public String reg()
{
System.out.println("执行了reg方法");
return "reg";
}
//这个没被拦截,可以执行
@RequestMapping("/login")
public String login()
{
System.out.println("执行了login方法");
return "login";
}
}
怎么排除所有的静态资源,就是比如图片,就不拦截,但是图片就有好几十种格式,难道我们要一个个excludePathPatterns吗?当然不是
我们可以在static下加一个image的目录,所有的图片放这个image里,然后再excludePathPatterns
拦截器实现流程
5.2统一异常处理
比如后端给前端返回的响应出错了,前端的页面很懵逼,如下图
前端这时候不知道是哪里出现了问题
这时候就要给异常做一个统一处理了,如果是空指针异常,就返回一种让前端看的懂的方式,如果是越界异常,就返回另一种方式
这时候就要统一异常处理了
统⼀异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件
下面代码是简单的处理了空指针异常
ExceptionAdvice代码
@RestControllerAdvice
public class ExceptionAdvice
{
@ExceptionHandler(NullPointerException.class)
public Result doNullPointerException(NullPointerException e)
{
Result result=new Result();
result.setCode(-1);
result.setMeg("空指针异常"+e.getMessage());
result.setData(null);
return result;
}
}
Result代码
@Data
public class Result
{
//无论什么时候,后端都返回给前端以下三个信息
private int code;//状态码
private String meg;//状态码的描述信息
private Object data;//返回数据
}
前端返回页面
但是上述只是空指针异常,而异常有很多,难道要我们一个一个写吗?
当然不是,所有异常的父类都是Expection
于是我们可以这么写
@ExceptionHandler(NullPointerException.class)
public Result doException(Exception e)
{
Result result=new Result();
result.setCode(-1);
result.setMeg("异常"+e.getMessage());
result.setData(null);
return result;
}
5.3统一数据返回格式
@Data
public class Result
{
//无论什么时候,后端都返回给前端以下三个信息
private int code;//状态码
private String meg;//状态码的描述信息
private Object data;//返回数据
//返回成功对象
public static Result success(Object data)
{
Result result=new Result();
result.setCode(200);
result.setMeg("");
result.setData(data);
return result;
}
public static Result fail(int code,String message)
{
Result result=new Result();
result.setCode(code);
result.setMeg(message);
result.setData(null);
return result;
}
}
package com.example.demo.config;
import com.example.demo.common.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 统一返回值的保底实现类
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/**
* true -> 才会调用 beforeBodyWrite 方法,
* 反之则永远不会调用 beforeBodyWrite 方法
*
* @param returnType
* @param converterType
* @return
*/
@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) {
// 已经包装好的对象
if (body instanceof Result) {
return body;
}
// 对字符串进行判断和处理
if (body instanceof String) {
Result resultAjax = Result.success(body);
try {
return objectMapper.writeValueAsString(resultAjax);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return Result.success(body);
}
}
@RestControllerAdvice
public class ExceptionAdvice
{
@ExceptionHandler(NullPointerException.class)
public Result doNullPointerException(NullPointerException e)
{
Result result=new Result();
result.setCode(-1);
result.setMeg("空指针异常"+e.getMessage());
result.setData(null);
return result;
}
@ExceptionHandler(NullPointerException.class)
public Result doException(Exception e)
{
Result result=new Result();
result.setCode(-1);
result.setMeg("异常"+e.getMessage());
result.setData(null);
return result;
}
}
6. @ControllerAdvice 源码分析(了解)
通过对 @ControllerAdvice 源码的分析我们可以知道上⾯统⼀异常和统⼀数据返回的执行流程,我们先从 @ControllerAdvice 的源码看起,点击 @ControllerAdvice 实现源码如下
从上述源码可以看出 @ControllerAdvice 派生于 @Component 组件,而所有组件初始化都会调用InitializingBean 接口。所以接下来我们来看 InitializingBean 有哪些实现类?在查询的过程中我们发现了,其中 Spring MVC中的实现子类是RequestMappingHandlerAdapter,它里面有⼀个方法 afterPropertiesSet() 方法,表示所有的参数设置完成之后执行的方法,如下图所示:
而这个方法中有⼀个 initControllerAdviceCache 方法,查询此方法的源码如下
我们发现这个方法在执行是会查找使用所有的@ControllerAdvice 类,这些类会被容器中,但发生某个事件时,调用相应的 Advice 方法,比如返回数据前调⽤统⼀数据封装,比如发生异常是调用异常的Advice 方法实现。