SpringAOP之动态代理
为什么要用代理?
代理可以在不改变源代码的基础上实现对代码的拓展
常用到的业务场景:
- 事务的处理
- 日志的打印
- 性能监控
- 异常的处理等
静态代理
业务类
/**
* 业务层接口
*/
public interface UserService {
void addUser();
void updateUser();
void deleteUser();
String getUser();
}
业务层中的具体实现类
package com.atguigu.service.impl;
import com.atguigu.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("用户新增成功");
}
@Override
public void updateUser() {
System.out.println("修改用户成功");
}
@Override
public void deleteUser() {
System.out.println("删除用户成功");
}
@Override
public String getUser() {
return "MrCui";
}
}
代理类:
/**
* 静态代理
*/
public class StaticProxy {
private static UserService userService = new UserServiceImpl();
// 代理方法
public static void addUser() {
try {
System.out.println("1-开启事务 ");
userService.addUser();
System.out.println("2-提交事务");
} catch (Exception e) {
System.out.println("3-回滚事务");
e.printStackTrace();
throw new RuntimeException(e);
} finally {
System.out.println("4-关闭事务");
}
}
}
静态代理类需要手动的写业务层中的每一个方法,会导致开发效率太低,代码冗余。为了解决这一痛点,出现了动态代理。
动态代理
目前动态代理有两种实现:
- 通过jdk原生代码来实现动态代理
- 通过cglib来实现动态代理
- 如何选择:
- jdk的动态代理 创建效率高 运行效率低
- Cglib的动态代理 创建效率低 运行效率高
- Spring官方默认使用的是jdk的动态代理,可以通过配置文件中的属性来修改代理规则。
JDK的动态代理:
注意事项及原理:
- 和代理对象需要有相同的接口
- 通过反射来实现动态代理
通过JDK的动态代理来实现:
public class MyJDKProxy {
/**
*
* @param target 需要被代理的目标对象
* @return
*/
public static Object proxy(Object target){
// 通过反射创建目标对象的代理对象,
// 参数分别是
// 类加载器
// 目标对象所实现的接口
// new InvocationHandler() 匿名内部类 这里面写的是具体的业务逻辑
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxy, method, args) -> {
Object result = null;
try {
System.out.println("开启事务");
// 可以在这里开启暴力反射
result = method.invoke(target,args);
System.out.println("提交事务");
}catch (Exception e){
System.out.println("回滚事务");
e.printStackTrace();
throw new RuntimeException();
}finally {
System.out.println("关闭事务");
}
return result;
});
}
}
CGlib动态代理
注意事项及原理:
- CGlib是通过继承目标类来实现代理目标方法的。
- CGlib是通过字节码文件来实现动态代理的,通过获取目标类的字节码文件来代理目标方法。
public class MyCglibProxy {
public static Object proxy(Object target){
Enhancer enhancer = new Enhancer();
// 获取目标类所实现的接口并设置代理类的接口
enhancer.setInterfaces(target.getClass().getInterfaces());
// 设置代理类的父类 即目标类
enhancer.setSuperclass(target.getClass());
enhancer.setCallback((InvocationHandler) (proxy, method, args) -> {
Object result = null;
try {
System.out.println("开启事务");
result = method.invoke(target,args);
System.out.println("提交事务");
}catch (Exception e){
System.out.println("回滚事务");
e.printStackTrace();
throw new RuntimeException(e);
}finally {
System.out.println("关闭事务");
}
return result;
});
// 创建代理对象
return enhancer.create();
}
}
通过Spring来实现动态代理
为什么要使用Spring来进行动态代理?
通过上述的操作我们已经可以实现了动态代理,但是目前还是有一些问题:
首先开发效率太低,其次我们无法来进行细节的处理。此时Spring官网为我们提供了代理即AOP
我们可以通过AOP来实现更好的动态代理。
如何使用Spring的动态代理
在学习Spring的动态代理之前首先我们需要了解一个关键词,这些关键词是Spring官方提供给我们,
帮我我们更好的理解Spring如何实现AOP规则的。
- Aspect(切面): 一个跨越多个类的关注点的模块化。事务管理是企业级Java应用中横切关注点的一个很好的例子。在Spring AOP中,切面是通过使用常规类(基于 schema 的方法)或使用 @Aspect 注解的常规类(@AspectJ 风格)实现的。
- Join point: 程序执行过程中的一个点,例如一个方法的执行或一个异常的处理。在Spring AOP中,一个连接点总是代表一个方法的执行。
- Advice: 一个切面在一个特定的连接点采取的行动。不同类型的advice包括 “around”、“before” 和 “after” 的advice(Advice 类型将在后面讨论)。许多AOP框架,包括Spring,都将advice建模为一个拦截器,并在连接点(Join point)周围维护一个拦截器链。
- Pointcut: 一个匹配连接点的谓词(predicate)。advice与一个切点表达式相关联,并在切点匹配的任何连接点上运行(例如,执行一个具有特定名称的方法)。由切点表达式匹配的连接点概念是AOP的核心,Spring默认使用AspectJ的切点表达式语言。
- Introduction: 代表一个类型声明额外的方法或字段。Spring AOP允许你为任何 advice 的对象引入新的接口(以及相应的实现)。例如,你可以使用引入来使一个bean实现 IsModified 接口,以简化缓存。(介绍在AspectJ社区中被称为类型间声明)。
- Target object: 被一个或多个切面所 advice 的对象。也被称为 “advised object”。由于Spring AOP是通过使用运行时代理来实现的,这个对象总是一个被代理的对象。
- AOP proxy: 一个由AOP框架创建的对象,以实现切面契约(advice 方法执行等)。在Spring框架中,AOP代理是一个JDK动态代理或CGLIB代理。
- Weaving(织入): 将aspect与其他应用程序类型或对象连接起来,以创建一个 advice 对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。Spring AOP和其他纯Java AOP框架一样,在运行时进行织入。
通知方式:
- Before advice: 在连接点之前运行的Advice ,但它不具备以下能力 阻止执行流进行到 join point 的能力(除非它抛出一个异常)
- After returning advice: 在一个连接点正常完成后运行的Advice (例如,如果一个方法返回时没有抛出一个异常)。
- After Throwing advice: 当一个匹配的方法执行通过抛出异常退出时,After throwing advice 运行。你可以通过使用 @AfterThrowing 注解来声明它。
- After (finally) advice: 无论连接点以何种方式退出(正常或特殊返回),都要运行该advice。
- Around advice: 围绕一个连接点的advice,如方法调用。这是最强大的一种advice。Around advice可以在方法调用之前和之后执行自定义行为。它还负责选择是否继续进行连接点或通过返回自己的返回值或抛出一个异常来缩短advice方法的执行。
步骤:
第一步: 首先需要引入依赖
<dependencyes>
<!--
spring 配置 aop依赖
spring-aspects 这两个依赖的版本要保持一致。
spring-context
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.20</version>
</dependency>
<!--spring核心配置依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
</dependency>
<!-- 单元测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencyes>
第二步: 编写AOP来实现动态代理
/**
* 基于Spring的Aop的动态代理。
* 基于Spring的Aop主要有5中通知
* 1.before 前置通知, 方法执行之前进行通知
* 2.afterReturning 返回通知, 方法执行后但还为返回结果此时进行通知
* 3.afterThrowing 异常通知, 方法执行时出现了异常则进行此通知。
* 4.after 后置通知 方法执行之后进行通知
* 5.around 环绕通知, 环绕通知可以控制目标方法是否调用。(最常用的通知)
*/
@Component
@Aspect
public class AopProxy {
/**
* 作用是用于标记
*/
@Pointcut("bean(userServiceImpl)")
public void pointCut(){}
/**
* 前置通知
*/
@Before("pointCut()")
public void before(){
System.out.println("1-前置通知");
}
@AfterReturning(value = "pointCut()",returning = "result")
public void afterReturning(Object result){
System.out.println("2-返回通知"+result);
}
@AfterThrowing(value = "pointCut()", throwing = "throwable")
public void afterThrowing(Throwable throwable){
System.out.println("3-异常通知"+throwable);
}
@After(value = "pointCut()")
public void after(){
System.out.println("4-后置通知 ----- 无论程序是否出错都会执行这个通知");
}
@Around(value = "pointCut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
// 环绕通知中 这行代码用来判断方法是否调用。
// 如果有通知 则执行通知,如果没有通知则执行目标方法。
joinPoint.proceed();
}
}
第三步: 在applicationContext.xml配置文件中配置aop
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--配置包扫描路径-->
<context:component-scan base-package="com.atguigu"/>
<!--
需要在bean中注册
proxy-target-class
false 表示 默认 使用的是 jdk的动态代理,如果代理类没有实现接口,那么spring会使用cglib来进行动态代理
true 表示使用的是 cglib的动态代理
-->
<aop:aspectj-autoproxy proxy-target-class="false"/>
</beans>
进行通过测试类 进行 测试
public class ProxyTest {
/**
* 测试静态代理
*/
@Test
public void testStaticProxy(){
StaticProxy.addUser();
}
/**
* 通过JDK动态代理 来实现动态代理
*/
@Test
public void testJDKProxy(){
UserService userService = new UserServiceImpl();
// 通过代理对象来调用目标方法。
UserService proxy = (UserService) MyJDKProxy.proxy(userService);
proxy.updateUser();
}
@Test
public void testCglibProxy(){
UserService userService = new UserServiceImpl();
UserService proxy = (UserService) CglibProxy.proxy(userService);
proxy.addUser();
}
@Test
public void testSpringProxy(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = context.getBean(UserService.class);
System.out.println(userService.getClass());
userService.addUser();
}
}
SpringAOP配置中需要注意的一些事项
- 当有两个代理类的时候我们需要设置代理类的代理顺序可以通过
@Order(1)
注解来调整代理顺序。这个注解在类名上添加,数字越小表示级别越高。 - Spring中当我们需要配置代理类代理多个类的时候可以通过
粒度
来进行配置- 粗粒度
第一种实现方式: 直接配置 如上方代码所示
第二种实现方式: within(“包名或类名”)within("com.demo.*")
// demo包下的所有的类名不包括子孙包within("com.demo..*")
// demo包下的所有的类名包括子孙包
- 细粒度
切入点表达式:exection( [权限修饰符] [返回类型] [类全路径] [方法名称] ([参数列表]))
exection(* com.demo.*.add*(..))
* 用于通配符 com.demo.* 表示用于匹配所有的类 .add*() 表示所有以add开头的方法, (..) 表示该方法可以携带参数也可以不携带参数。
- 粗粒度