环境:Spring6.1.2
1. 简介
在Spring框架中,AOP(面向切面编程)是一种强大的编程范式,它允许开发者在不修改原有代码的情况下,为程序添加额外的功能,如日志记录、事务管理、安全控制等。
实际开发中常用实现AOP配置方式:
- 基于XML
在早期的Spring版本中,开发者常常使用XML配置文件来定义切面、通知和目标对象之间的关联。通过配置、、等标签,可以轻松地实现AOP的各种功能。如下示例:
- 基于注解
通过在切面类和方法上使用如@Aspect、@Before、@After等注解,可以更加简洁地定义AOP的相关配置。这种方式不仅减少了XML配置的工作量,还使得代码更加清晰易读。如下示例:
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* save(..))")
private void logPc() {}
@Around("logPc()")
public Object process(ProceedingJoinPoint pjp) throws Throwable {
Object ret = null ;
System.out.println("before log...") ;
ret = pjp.proceed() ;
System.out.println("after log...") ;
return ret ;
}
}
以上是Spring提供的2中方式来声明AOP配置方式。但如果你需要一种更加灵活和可配置性,那么Spring还提供了一个非常方便强大的ProxyFactoryBean类,该类特别适合那些需要更多自定义和控制的场景,例如当你需要为特定的Bean创建代理,或者需要在不修改原始代码的情况下为现有类添加额外的功能时。
2. 实战案例
ProxyFactoryBean与其他Spring FactoryBean实现一样,引入了一个间接级别。如果定义了名为pack的ProxyFactoryBean,那么引用pack的对象看不到ProxyFactoryBean实例本身,而是由ProxyFactoryBean#getObject()方法实现创建的对象。此方法创建一个AOP代理,用于包装目标对象。
2.1 属性配置
ProxyFactoryBean提供了很多属性,让你可以灵活的配置代理对象。该对象继承了ProxyConfig,一些关键的属性是由ProxyConfig定义。
- proxyTargetClass:如果要代理目标类,而不是目标类的接口,则为true。如果此属性值设置为true,则会创建CGLIB代理。
- optimize:控制是否对通过CGLIB创建的代理应用积极的优化。除非完全理解相关AOP代理如何处理优化,否则不应该轻松地使用此设置。目前仅用于CGLIB代理。它对JDK动态代理没有影响。
- frozen:如果代理配置被冻结,则不再允许更改该配置。此属性的默认值为false,因此允许更改(例如添加额外的通知)。
- exposeProxy:确定是否应在ThreadLocal中公开当前代理,以便目标可以访问它。如果目标需要获取代理,并且exposeProxy属性设置为true,则该目标可以使用AoPontext.currentProxy()方法获取代理对象。
- proxyInterface:字符串接口名称的数组。
- interceptorNames:要应用的Advisor、拦截器或其他建议名称的字符串数组。
接下来将从2方面介绍ProxyFactoryBean的使用,代理接口与代理类。2.2 代理接口
要通过ProxyFactoryBean创建代理,你至少需要涉及到下面几点(类):
- 需要被代理的目标bean类。
- 一个Advisor或者Advice,增强部分。
- 指定要代理的接口。
如下示例:
public interface ICommonDAO {
void save() ;
}
@Component("commonDAOTarget")
public class CommonDAOImpl implements ICommonDAO {
@Override
public void save() {
System.out.println("save operator...") ;
}
}
@Component
public class LogInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("before log...") ;
Object ret = invocation.proceed() ;
System.out.println("after log...") ;
return ret ;
}
}
@Configuration
public class AppConfig {
@Bean
// 由于上面已经定义了CommonDAOImpl,而这里的FactoryBean#getObject返回的
// 也是一个实现了ICommonDAO接口的对象,所以需要加上@Primary
@Primary
ProxyFactoryBean commonDAO(@Qualifier("commonDAOTarget") CommonDAOImpl commonDAOTarget) throws Exception {
ProxyFactoryBean proxy = new ProxyFactoryBean() ;
proxy.setProxyInterfaces(new Class[] {ICommonDAO.class}) ;
proxy.setTarget(commonDAOTarget) ;
proxy.setInterceptorNames("logInterceptor") ;
return proxy ;
}
}
测试
ICommonDAO dao = context.getBean(ICommonDAO.class) ;
dao.save() ;
// 输出
before log...
save operator...
after log...
2.3 代理类
如果我们的目标没有实现接口,那么我们只能通过CGLIB进行代理,通过设置proxyTargetClass属性为true。CGLIB代理通过在运行时生成目标类的子类来工作。Spring将这个生成的子类配置为将方法调用委托给原始目标。如下示例:
@Component("commonDAOTarget")
public class CommonDAO {
public void save() {
System.out.println("save operator...") ;
}
}
@Bean
@Primary
ProxyFactoryBean commonDAO(@Qualifier("commonDAOTarget") CommonDAO commonDAOTarget) throws Exception {
ProxyFactoryBean proxy = new ProxyFactoryBean() ;
proxy.setTarget(commonDAOTarget) ;
proxy.setInterceptorNames("logInterceptor") ;
// 代理类,可以不设置
proxy.setProxyTargetClass(true) ;
return proxy ;
}
查看最终的CommonDAO是否是通过CGLIB代理
CommonDAO dao = context.getBean(CommonDAO.class) ;
System.out.println(dao.getClass()) ;
输出结果
class com.pack.aop.create.ProxyFactoryBeanTest2$CommonDAO$$SpringCGLIB$$1
CGLIB代理通过在运行时生成目标类的子类来工作。但需要注意以下事项:
- final 类不能被代理,因为它们不能被扩展。
- final方法无法提供增强,因为它们不能被覆盖。
- 不能增强private方法,因为它们不能被重写。
- 不可见的方法,通常是来自不同包的父类中的包私有方法,不能被增强,因为它们实际上是私有的。
2.4 模糊匹配拦截器
在上面配置拦截器时,我们都是指定的具体拦截器,其实我们还可以使用通配符,指定拦截器。如下示例:
@Component("global_log")
public class LogInterceptor implements MethodInterceptor {
}
@Component("global_auth")
public class AuthInterceptor implements MethodInterceptor {
}
// ProxyFactoryBena配置
ProxyFactoryBean commonDAO() throws Exception {
ProxyFactoryBean proxy = new ProxyFactoryBean() ;
// 注意:这里的通配符必须是最后,你不能放到其它位置
proxy.setInterceptorNames("global_*") ;
return proxy ;
}
以上ProxyFactoryBean在初始化时,会自动查找容器中beanName以global_开头的所有Bean对象。