什么是AOP思想
Spring 框架通过定义切面, 通过拦截切点实现了不同业务模块的解耦,这个就叫面向切面编程 - Aspect Oriented Programming (AOP)
spring将AOP的思想引入框架之中,通过预编译方式和运行期间动态代理实现程序的统一维护
简单来说,AOP是一种面向切面编程的思想,是对某一种事情的集中处理。
比如查询订单、后台管理等操作,都需要进行登录鉴权,没有SpringAOP框架,就需要在多个controller层代码中重复增加判断用户登录的代码,而有了SpringAOP框架,可以通过简单的注解调用判断用户是否登录的方法,从而让逻辑上相关的多个业务解耦合
AOP是一种思想,而SpringAOP是框架,提供了对AOP思想的实现
AOP和OOP的区别
OOP是面向对象编程,对实体的行为和属性进行抽象封装,以更好的描述某一个实体
AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,来解开逻辑上存在相关性的事物的耦合性
AOP
AOP术语
切面:定义的AOP是关于哪一个功能的,这个功能就是切面。比如用户登录功能,日志记录功能;切面由切点和通知组成
连接点:所有可能触发AOP定义的拦截规则的点,都是连接点
切点:定义AOP的拦截规则
通知:执行AOP执行的时间和执行的方法,包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice)异常通知(After throwing advice)最终通知(After (finally) advice)
![](https://img-blog.csdnimg.cn/img_convert/df4e377259724005bdcc04d3d12e120a.png)
通知类型
前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
返回通知(After returning advice):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
异常通知(After throwing advice):在方法抛出异常退出时执行的通知。
后置通知(After advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
环绕通知(Around Advice):包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。
AspectJ
AspectJ是一个java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)
Spring AOP和AspectJ是什么关系?
AspectJ是更强的AOP框架,是实际意义的AOP标准,旨在提供完整的AOP解决方案。
Spring AOP使用纯Java实现,Spring AOP旨在 在Spring IoC上提供一个简单的AOP实现,以解决程序员面临的最常见问题。
Spring AOP兼容了AspectJ语法
项目创建
导入依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.8</version>
</dependency>
定义一个切面
@Aspect:声明这是一个切面类(使用时需要与@Component注解一起用,表明同时将该类交给spring框架管理)
@Component泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller、@Services等的时候),我们就可以使用@Component来标注这个类,这个类就会被注入进入Spring框架
@Aspect //声明这个类是一个切面类
@Component //将这个类交给Spring框架,这个拦截类肯定要比使用类提早或者同时创建,所以说在项目启动时是应该创建的
public class UserAspect {
}
定义切点
@Pointcut:定义一个切点,并设置拦截规则,切点方法不需要设置具体方法内容
@Aspect //声明这个类是一个切面类
@Component //将这个类交给Spring框架,这个拦截类肯定要比使用类提早或者同时创建,所以说在项目启动时是应该创建的
public class UserAspect {
//定义一个切点
@Pointcut("execution(* com.example.demo.Controller.UserController.* (..))")
public void pointcut(){
}
}
@Controller
@RequestMapping("/UserController")
public class UserController {
@RequestMapping("/get")
@ResponseBody
public String get(){
System.out.println("执行get方法");
return "hello world";
}
@RequestMapping("/set")
@ResponseBody
public void set(int x,int y) {
System.out.println("执行set方法,x= " + x + " y=" + y);
}
}
AspectJ的通配符
* 匹配任意字符,只匹配一个元素,可以用在返回值,包名,类名,方法名上
..: 匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用可以使用在方法参数上,如果使用在类上要配合*使用
+ 表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.test.Animal+ ,表示继承该类的所有子类包括本身
"execution(* com.example.demo.Controller.UserController.* (..))")“这是AspectJ语法,用于定义切点表达式
具体格式为
execution(修饰符 返回值 包.类.方法名(参数) throws异常)
其中:除了返回类型、方法名和参数外,其它项都是可选的。
修饰符,比如public,private等,但是一般省略,默认匹配任意修饰符
返回值,不能省略。String就表示返回类型是String,void表示没有返回值,*表示匹配任意返回值
包名:可以省略。比如com.demo,就表示匹配com.demo这个包;com.demo..表示匹配com.demo这个包和包下的所有子包(..匹配多个)
类名:可以省略。比如cat,匹配cat类;*cat,匹配以cat结尾的类;cat*,匹配以cat开头的类;*匹配任意类
方法名:不能省略。比如addUser,匹配这个方法;add*匹配以add开头的方法
方法参数:不能省略()表示无参方法,(int,int)表示参数是int,int的方法 ,(..)表示匹配任意参数
比如:
![](https://img-blog.csdnimg.cn/img_convert/d7755d4ef8d9437bb6dd835743357467.png)
![](https://img-blog.csdnimg.cn/img_convert/92929a7c839b43f9bfa8b10f95ebefec.png)
![](https://img-blog.csdnimg.cn/img_convert/682c4acd223f4c77914f7758b4a05d76.png)
4、定义通知:设置切点的执行时间和方法
1、前置通知:在某连接点之前执行的通知
@Before注解,在里面填入具体执行的切点方法名称
![](https://img-blog.csdnimg.cn/img_convert/3e65b23c2fc542e6bd2686a081a928a5.png)
![](https://img-blog.csdnimg.cn/img_convert/c282ee71f471482f964ef34fa75934f3.png)
2、后置通知:在某连接点之后执行的通知
@After注解
//定义前置方法 在符合切点规则时的类方法被执行前使用
@Before("pointcut()")
public void doBefore() {
System.out.println("调用了前置方法");
}
//定义后置方法 在符合切点规则时的类方法被执行后使用
@After("pointcut()")
public void doAfter() {
System.out.println("调用了后置方法");
}
![](https://img-blog.csdnimg.cn/img_convert/9ab38cac2e54419399a9ec3fbb06119d.png)
3、返回通知
@AfterReturning在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
而@After定义的是在某连接点退出后执行的通知:不论是异常退出或者正常退出
@AfterReturning("pointcut()")
public void doAfterReturning() {
System.out.println("返回结果后通知");
}
比如加上一个除0异常
![](https://img-blog.csdnimg.cn/img_convert/601954682c8f41f8b95068f649e5dec4.png)
![](https://img-blog.csdnimg.cn/img_convert/f84ac084a7624434afb7911502814d0b.png)
运行结果可以看出:没有运行AfterReturning注解修饰的方法,但是执行了后置通知
去掉异常,结果是:
![](https://img-blog.csdnimg.cn/img_convert/2425c34618324c1db0469ad9122525e3.png)
也就是说:After(后置通知)无论程序异常或者正常结束,都会运行;而返回通知只会在程序正常结束后运行
4、异常通知:捕捉到了异常,执行的通知
@AfterThrowing("pointcut()")
public void doAfterThrowing() {
System.out.println("捕捉到了异常");
}
![](https://img-blog.csdnimg.cn/img_convert/8db11a295bff449bbc83510a4c49f752.png)
5、环绕通知:环绕通知可以在方法调用前后完成自定义的行为
使用时,方法要使用ProceedingJoinPoint对象
@Around("pointcut()")
//Proceedingjoinpoint 继承了JoinPoint,在JoinPoint的基础上暴露出 proceed(),
public void doAround(ProceedingJoinPoint proceedingJoinPoint){
try {
System.out.println("doAround执行");
proceedingJoinPoint.proceed();
System.out.println("doAround结束");
} catch (Throwable e) {
e.printStackTrace();
}
}
![](https://img-blog.csdnimg.cn/img_convert/a59dd1e732264b47809ec48d0db8e04c.png)
调试,发现proceedingJoinPoint可以获取连接点信息
![](https://img-blog.csdnimg.cn/img_convert/1b1e75d1a62049e2b969158d071f2994.png)
应用:使用AOP统计每一个方法的运行时间
@Around("pointcut()")
//Proceedingjoinpoint 继承了JoinPoint,在JoinPoint的基础上暴露出 proceed(),
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) {
StopWatch stopWatch = new StopWatch();//实现时间统计
Object result = null;
try {
stopWatch.start();//开始计时
result = proceedingJoinPoint.proceed();
stopWatch.stop();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println(proceedingJoinPoint.getSignature().getDeclaringTypeName() + " "
+ proceedingJoinPoint.getSignature().getName() + "方法执行花费了" +
stopWatch.getTotalTimeMillis() + "ms");
return result;
}
@Controller
@RequestMapping("/UserController")
public class UserController {
@RequestMapping("/get")
@ResponseBody
public String get() throws InterruptedException {
Thread.sleep(200);
System.out.println("执行get方法");
return "hello world";
}
@RequestMapping("/set")
@ResponseBody
public String set( ) throws InterruptedException {
Thread.sleep(300);
System.out.println("执行了set方法");
return "hi";
}
}
![](https://img-blog.csdnimg.cn/img_convert/91b204eb9b1a48cfa9626ef6bde92406.png)
Spring AOP的实现原理
Spring的AOP就是通过动态代理实现的。如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
动态代理
有一位歌手要唱歌,需要有搭建舞台,收取门票费,歌手唱歌等步骤。
但是这样,这位歌手就要做好多事情,她就可以找一个经纪公司,让经纪人负责完成前期的准备工作,歌手只负责唱歌就好
![](https://img-blog.csdnimg.cn/img_convert/1a948c56e51c4823a84bae0cb028ee1d.png)
Spring AOP基于动态代理实现,调用者调用的是代理类,然后代理类去调用目标方法
JDK的动态代理
Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。
JDK的动态代理通过反射实现
JDK实现动态代理需要两个组件:
InvocationHandler接口,定义的类要实现invoke()方法,也就是代理方法
通过Proxy类的newProxyInstance()方法,创建代理对象
JDK通过反射,生成一个代理类,这个代理类(Proxy类的子类)实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler接口的invoke方法。
CGLib的动态代理
CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,加入我们定义的AOP方法
两种方式的对比
JDK动态代理是JDK原生的,不需要任何依赖即可使用;
JDK方式,通过反射机制生成代理类,速度要比CGLib操作字节码生成代理类的速度更快;
JDK方式,目标类要实现接口,同时JDK动态代理无法为没有在接口中定义的方法实现代理
使用CGLib代理的类,代理类是目标类的子类,可以对目标类中可重写的方法进行代理
CGLib方式的原理是继承,也就是说 目标类不可以被final修饰,同时,目标类中不可重写的方法也不会被代理
JDK通过反射执行方法比CGLib直接调用方法的速度慢
织入
织入是将切面应用到目标对象并创建代理对象的过程
切面在某一个连接点处,会被织入到目标对象中,这个时机有多种
编译期:切面在目标类编译时被织入,这种方式需要特点的编译器(AspectJ的织入方式)
类加载期:切面在目标类加载入JVM时被织入。这种需要特殊的类加载器(AspectJ5的织入方式)
运行期:切面在应用运行期间织入。在织入对象时,AOP容器会动态创建代理对象(Spring AOP方式)
统一事件处理
Spring AOP可以实现统一登录验证,异常处理、统一格式返回等
拦截器
什么是拦截器:在AOP中用于在某个方法或字段被访问之前,进行拦截,然后在之前或之后加入某些操作。拦截是AOP的一种实现策略
实现方法:类实现HandlerInterceptor接口,并重写方法,其内部方法如下:
preHandle:在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制、权限校验等处理;
postHandle:在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView
afterCompletion:在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面);
创建拦截器:实现HandlerInterceptor接口,重写preHandle方法
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session=request.getSession();
if(session==null||session.getAttribute("user")==null)
return false;
return true;
}
}
将拦截器配置到系统设置:实现WebMvcConfigurer接口,重写addInterceptors方法
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/login.html")//登录页面
.excludePathPatterns("/user/login");//登录接口
}
}
addPathPatterns()表示在 LoginInterceptor对象的preHandle()方法返回false时,拦截的方法,**表示所有
excludePathPatterns()表示不拦截的方法
在没有加入拦截器之前,可以访问url:
![](https://img-blog.csdnimg.cn/img_convert/972f0adfe32f456fb5e4cd0aa2f5b762.png)
而配置拦截器之后,只可以访问login.html,其他路径在用户没有登录时都是不可访问的
用户没有登录:
这时,这可以访问login.html页面,并进行用户登录。但是其他路径都不可以访问
![](https://img-blog.csdnimg.cn/img_convert/efcb3ee2237b4191a0d41f2bb606f411.png)
在登录成功之后,可以访问其他接口
![](https://img-blog.csdnimg.cn/img_convert/3221007068984e8e8067d709843f9ed3.png)
在Controller层方法执行之前,调用预处理方法,而在预处理方法中获取到了所有的拦截器对象,并执行拦截器里的preHandle()方法
异常处理
当后端产生了各种意料之外的异常,如果没有对异常数据进行统一处理,前端展示的可能就是用户看不懂的信息
比如:后端空指针异常,前端展示的会是
![](https://img-blog.csdnimg.cn/img_convert/c97f38e5609e409e8749a4e58b3e5e58.png)
@ControllerAdvice//控制器通知类
public class ErrorAdvice {
@ExceptionHandler(Exception.class)
//异常处理器
//Exception.class 表示接受所有的异常
@ResponseBody
public Object errorAdvice(Exception e) {
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("meg", e.getMessage());
hashMap.put("state", 1);
return hashMap;
}
}
![](https://img-blog.csdnimg.cn/img_convert/657a90da11fd4cbdb3bb2a32c4776de0.png)
统一数据返回
将后端数据以同一种格式返回,这样前端获取到的大量数据,都会是某一个固定的格式(一般是json格式),方便处理
ResponseBodyAdvice接口,是对Controller返回的数据, 进行相应的处理操作后,再将结果返回给客户端
@ControllerAdvice//控制器通知类
public class ResponseAdvice implements ResponseBodyAdvice {
//supports表示是否要统一数据,如果返回true,表示要将数据构造为统一的格式,也就是进入了beforeBodyWrite方法
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// if (returnType.getMethod() != null)
// System.out.println(returnType.getMethod().getName());//获取方法名称
//returnType.getParameterType().isAssignableFrom(ErrorAdvice.class);//判断连接点的返回值是否和 ErrorAdvice类 有父子类关系
//getParameterType方法主要是获取参数的类型
// isAssignableFrom是用来判断子类和父类的关系的
//1、如果返回的数据 是User对象 不需要统一格式
return !returnType.getParameterType().isAssignableFrom(User.class);
}
//统一格式的处理
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
System.out.println("body " + body);
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("统一处理 数据部分", body);
hashMap.put("统一处理 state", 1);
ObjectMapper objectMapper = new ObjectMapper();//引入jackson依赖
// 这里需要进行特殊处理,因为 String 在转换的时候报错
if (body instanceof String) {
try {
return objectMapper.writeValueAsString(hashMap);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return hashMap;
}
}
通过调试可以看到supports()方法的参数
![](https://img-blog.csdnimg.cn/img_convert/6bc9bfd433c549a693ba2470dacd6853.png)
这里,要特殊对String数据进行转化,否则会报错:
![](https://img-blog.csdnimg.cn/img_convert/29eb4a4a5f994de096fec3f561d36844.png)
![](https://img-blog.csdnimg.cn/img_convert/5544260ca0874e6288c5986f9fb0c134.png)
@Controller
@RequestMapping("/UserController")
public class UserController {
@RequestMapping("/get")
@ResponseBody
public String get() {
System.out.println("执行get方法");
return "hello world";
}
@RequestMapping("/set")
@ResponseBody
public User set() {
System.out.println("执行了set方法");
User user = new User();
user.setSno("9999");
user.setName("李四");
return user;
}
}
![](https://img-blog.csdnimg.cn/img_convert/41fdbb54c3b1469bbe5a8b332be3e8da.png)
![](https://img-blog.csdnimg.cn/img_convert/ddb00e2e02c8495e900bb0356112c778.png)