动态代理
代理模式:给一个对象提供一个代理,并由代理对象来控制真实对象的访问(调用者并不知道真实对象是什么)。
代理模式分静态代理和动态代理。这里只讨论动态代理,通俗的讲,动态代理就是在不修改代码的基础对被代理对象进行方法的增强。
基于接口的动态代理
JDK自带的动态代理就是基于接口的动态代理,被代理对象至少要实现一个接口,否则就无法使用代理。底层还是基于Java的反射来创建代理对象的。
JDK动态代理主要涉及两个类,java.lang.reflect.Proxy
和 java.lang.reflect.InvocationHandler
。下面来看个例子。
被代理类的接口IProduct
public interface IProduct { void saleProduct(Float money); } 复制代码
被代理类Product实现上面这个接口
public class Product implements IProduct{ @Override public void saleProduct(Float money) { System.out.println("卖出一个产品,收到金额:" + money); } } 复制代码
被代理类Product实现上面这个接口
public class Product implements IProduct{ @Override public void saleProduct(Float money) { System.out.println("卖出一个产品,收到金额:" + money); } } 复制代码
编写一个客户端类获取一个Product的动态代理的对象,来控制被代理对象的访问。 InvocationHandler的实现主要通过内部类实现(可以直接使用lambda表达式更简洁)。
public class Client { public static void main(String[] args){ Product product = new Product(); // 基于接口的动态代理(jdk自带的) // proxyProduct就是Proxy.newProxyInstance生成的代理对象,需要强转为被代理对象的接口类。 // 参数: // ClassLoader:代理对象的类加载器。使用和被代理对象相同的类加载器,直接调用getClass().getClassLoader()获取。 // Class<?>[]:被代理对象的接口的字节码。直接调用getClass().getInterfaces()获取就行 // InvocationHandler:进行代码的增强。直接使用内部类或者new一个InvocationHandler的实现类(在实现类中进行代码增强) IProduct proxyProduct = (IProduct) Proxy.newProxyInstance(product.getClass().getClassLoader() , product.getClass().getInterfaces(), new InvocationHandler() { /** * 进行代理增强的方法 * 参数: * proxy:当前的代理对象 * method:被代理对象当前执行的方法 * args:被代理对象当前执行的方法的参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object res = null; // 获取方法执行的参数 Float money = (Float)args[0]; // 判断当前方法是否是销售方法 if ("saleProduct".equals(method.getName())){ // 返回值与被代理对象方法的相同 res = method.invoke(product, money * 0.8f); } return res; } }); // 调用这个代理对象,执行被代理对象的方法。 proxyProduct.saleProduct(1000f); } } 复制代码
基于子类的动态代理
CGLIB动态代理是一个基于ASM字节码操作框架的代码生成库,它被广泛用于基于代理的AOP框架提供方法拦截。
本质上说,对于需要被代理的类,它只是动态的生成一个子类以覆盖非final的方法(所以它不能代理final类或final方法),同时绑定钩子回调自定义的拦截器。它比使用Java反射的JDK动态代理方法更快。
CGLIB动态代理的核心是net.sf.cglib.proxy.Enhancer类(用于创建被代理对象子类),net.sf.cglib.proxy.MethodInterceptor是最常用的回调类型(它是net.sf.cglib.proxy.Callback的一个子接口)。
<!--cglib动态代理的依赖--> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.12</version> </dependency> 复制代码
下面来看一个示例(被代理对象还是和上面的相同,这里实现一个客户端类)。
public class Client { public static void main(String[] args){ Product product = new Product(); /** * 基于子类的动态代理(cglib) * Enhancer的create方法,通过Enhancer来创建代理对象,MethodInterceptor来增强方法 * 参数: * Class:被代理对象的字节码 * Callback:用于增强方法。一般使用这个接口的子接口MethodInterceptor。 */ Product proxyProduct = (Product) Enhancer.create(product.getClass(), new MethodInterceptor() { /** * 执行被代理对象的任何方法都会经过此方法 * @param proxy 当前代理对象 * @param method 被代理对象要执行的方法 * @param args1 要执行的方法的参数 * 以上三个参数与基于接口的动态代理(jdk)的参数一致 * @param methodProxy 当前执行方法的代理对象 一般使用这个对象来调用原始对象的方法,因为它性能更高 * @return 与被代理对象要执行的方法的返回值一致 * @throws Throwable */ @Override public Object intercept(Object proxy, Method method, Object[] args1, MethodProxy methodProxy) throws Throwable { Object res = null; // 获取方法执行的参数 Float money = (Float)args1[0]; // 判断当前方法是否是销售方法 if ("saleProduct".equals(method.getName())){ // 修改原始方法的参数 args1[0] = money * 0.8f; // 与JDK动态代理的一个区别,使用MethodProxy对象来调用原始对象的方法 res = methodProxy.invoke(product, args1); } return res; } }); // 通过代理对象执行被代理对象的方法 proxyProduct.saleProduct(1000f); } } 复制代码
spring AOP
spring AOP是基于动态代理实现的,spring默认使用JDK动态代理实现AOP,也可以强制使用CGLIB。AOP是IoC的一种补充,IoC不依赖与AOP,但是AOP依赖与IoC。
AOP的一些概念
- JoinPoint(连接点):指被拦截到的点。在spring AOP中,就是指那些被拦截的方法。
- PointCut(切入点):是指筛选出的连接点(因为所有的切入点都是连接点,但是有的连接点并不需要进行拦截增强方法)
- Advice(通知/增强):需要完成的工作叫做通知,就是写的业务逻辑代码中的公共代码,比如事务、日志等。通知的类型有五种。
- Before(前置通知):在连接点(拦截的方法)执行前的通知。
- AfterReturning(后置通知):在连接点正常完成返回后的通知。
- AfterThrowing(异常通知):如果方法执行过程中抛出了异常,则执行此通知。
- After(最终通知):无论方法执行是否成功,最终都要执行这个通知。
- Around(环绕通知):围绕连接点的通知,它是最常用的通知,因为它可以包括以上四种通知。
- Aspect(切面):其实就是通知和切点的结合,通知和切点共同定义了切面的全部内容,它是干什么的,什么时候在哪执行。
- Introduction(引入):在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。
- Target(目标):被通知的对象。也就是需要加入额外代码的对象,也就是真正的业务逻辑被组织织入切面。
- Weaving(织入):切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点(编译期,类加载期,运行期)可以进行织入,spring是在运行期织入的。
@Aspectj支持
Spring 使用 AspectJ 提供的 library 解释与 AspectJ 5 相同的注释,用于切入点解析和匹配。但是,AOP 运行时仍然是纯粹的 Spring AOP,并且不依赖于 AspectJ 编译器或编织器。(也就是spring只是利用Aspectj来对切入点进行解析和匹配,真正的AOP的执行还是使用Spring AOP)。
在springboot中使用spring AOP非常的方便,只需引入一个spring支持AspectJ的starter依赖即可。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> 复制代码
在spring中也很简单,在引用spring AOP的基础上再引入一个aspectjweaver依赖就行。
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.2</version> </dependency> 复制代码
在spring中,要使用 Java @Configuration启用 @AspectJ 支持,还需添加@EnableAspectJAutoProxy注解(在springboot中不用)。
下面来看一个简单的示例(基于springboot)。
先写个Service类(包括接口和实现类)。
public interface AccountService { Account findById(Integer aid); } @Service public class AccountServiceImpl implements AccountService { private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class); @Override public Account findById(Integer aid) { log.info("正在执行方法"); return accountMapper.findById(aid); } } 复制代码
一个配置类,对service中的方法进行切面。
@Aspect // 声明切面, 这个bean将被spring自动检测,并用于配置Spring AOP @Component // 将这个类注册为一个bean,交给spring来管理(所以spring AOP依赖于IoC) public class Logger { private static org.slf4j.Logger log = LoggerFactory.getLogger(Logger.class); /** * 通用化切入点表达式,其他切入点可以直接引用这个 */ @Pointcut("execution(* tkmybatis.service.impl.*.*(..))") public void pointCut(){ } /** * 前置通知,在切入点方法执行之前执行 */ @Before("pointCut()") public void printLogBefore(){ log.info("我在方法执行前"); } /** * 后置通知,方法正常返回后执行 */ @AfterReturning("pointCut()") public void printLogAfterReturning(){ log.info("我在方法执行正常返回后"); } /** * 异常通知,方法发生异常时执行 */ @AfterThrowing("pointCut()") public void printLogAfterThrow(){ log.info("我在方法执行抛出异常后"); } /** * 最终通知,无论方法是否执行正常,它都会在最后执行 */ @After("pointCut()") public void printLogAfter(){ log.info("我在方法执行最后"); } @Around("pointCut()") public Object printAround(ProceedingJoinPoint pjp){ Object res; try{ Object[] args = pjp.getArgs(); log.info("我在方法执行前"); res = pjp.proceed(args); log.info("我在方法执行正常返回后2"); return res; }catch (Throwable t){ log.info("我在方法执行抛出异常后2"); throw new RuntimeException(t); }finally { log.info("我在方法执行最后"); } } } 复制代码
对于切入点表达式execution(* tkmybatis.service.impl.*.*(..))
,这里做一下解释。
execution表达式本来有三个部分(可见类型 返回值 连接点的全限定名),这里的可见类型可以省略。
- *代表返回值可以为任何值。
- tkmybatis.service.impl代表service实现类所在的包(进一步还可以直接用* .来替代,表示所有包)。
- .* 代表这个包下的所有类。下一个.* 代表这个类下的所有方法(还可以用方法前缀来表示,比如find*)。
- 括号里的 .. 代表参数可以为任意参数,可为多个或无。
更多高级用法详见Spring官方文档 Spring AOP
Spring 事务管理
Spring支持声明式的事务管理和编程式的事务管理。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。Spring推荐使用声明式的事务管理。
而声明式的事务管理就是基于Spring AOP的,其本质就是对要执行的方法进行拦截,在方法执行前加入一个事务,方法执行完成后根据情况判断是提交事务还是回滚事务(抛出了异常)。
事务的隔离级别
通过org.springframework.transaction.annotation.Isolation 这个枚举类来指定。
public enum Isolation { // 这是默认值,表示使用底层数据库的默认隔离级别。对于MySQL的InnoDB存储引擎,它是REPEATABLE_READ,其它一般的都是READ_COMMITTED DEFAULT(-1), // 跟没有一样,几乎不使用。 READ_UNCOMMITTED(1), // 只能读取另一个事务已提交的事务,能防止脏读。也是一般数据库的默认隔离级别。 READ_COMMITTED(2), // 可重复读(在一个事务内多次查询的结果相同,其它事务不可修改该查询条件范围内的数据,相当于加了个读锁) REPEATABLE_READ(4), // 所有的事务依次逐个执行,相当于串行化了,效率太低,一般也不使用。 SERIALIZABLE(8); } 复制代码
事务的传播行为
如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。org.springframework.transaction.annotation.Propagation枚举类中定义了7表示传播行为的枚举值。
public enum Propagation { // 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 REQUIRED(0), // 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 SUPPORTS(1), // 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 MANDATORY(2), // 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 REQUIRES_NEW(3), // 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 NOT_SUPPORTED(4), // 以非事务方式运行,如果当前存在事务,则抛出异常。 NEVER(5), // 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED。 NESTED(6); } 复制代码
声明式事务的使用
声明式事务可以直接使用@Transactional注解(这里只讨论这个)来配置,当然也可以使用XML配置文件。
先来看一下@Transactional注解都有啥
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { // 别名为transactionManager,其实这两是同一个。就是事务的名字。 @AliasFor("transactionManager") String value() default ""; // 事务的传播行为,默认值为REQUIRED Propagation propagation() default Propagation.REQUIRED; // 事务的隔离级别,默认为默认值(也就是使用底层数据库的隔离级别) Isolation isolation() default Isolation.DEFAULT; // 超时时间,默认为 -1,也就是没有超时时间。 int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; // 是否只读,默认为false。 boolean readOnly() default false; // 会触发事务回滚的异常的字节码数组 Class<? extends Throwable>[] rollbackFor() default {}; // 不会触发事务回滚的异常的字节码数组 Class<? extends Throwable>[] noRollbackFor() default {}; } 复制代码
这里直接在实现类上使用,这个注解还可以在接口(一般不在接口上使用)和方法上使用(可见性必须为public,否则会被忽略)。
如果是在spring中使用,需要在配置类中加上@EnableTransactionManagement注解(springboot则不需要)。proxy-target-class属性可以控制动态代理的类型,如果值为false或忽略此属性,则使用JDK的动态代理,可以设置为true以强制使用CGLIB来进行动态代理(如果被代理的类没有实现接口,将会自动使用CGLIB进行动态代理)。
@Service @Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) // 这里触发回滚的异常设置为Exception public class AccountServiceImpl implements AccountService { } 复制代码
参考: