阅读本文,你将了解到如何使用Spring AOP及AOP的基本原理,文末还与大家分享AOP的使用情景
在面向对象编程中(OOP)中,我们编程的关注点在于某个对象实体有哪些具体功能及其子类功能实现的不同。不同于OOP,面向切面编程(AOP)更多关注的业务流程。在不侵入业务代码的前提下,我们可以通过AOP编程,为业务流程某个具体环节(连接点)增加业务逻辑(通知),这些业务逻辑可能是打印日志、安全控制、事务控制等等。
使用AOP前必须理解清楚AOP相关的几个概念:通知(Advice)、切点(pointcut)、切面(aspect)、。
- 通知(Advice):通知要解决的是通知什么、什么时候通知的问题。通知什么指的我们增加的功能,比如日志打印、事务控制等。什么时候通知指的是我们在什么时候调用我们增加的功能。我们可以在方法调用前调用通知(Before)、方法调用后调用通知(After)、方法调用成功后调用通知(After-returning)、方法调用异常后调用通知(After-throwing)、在方法调用前和调用后调用通知(Around)
- 切点(Piontcut):切点主要定义的是在什么位置上应用通知,SpringAOP仅支持方法级别的切面编程(这和其应用动态代理实现有关)。一般我们会指定某个类的某个方法为切点,或者匹配某一通配符的一个或多个方法为切点,还可以指定由某一注解修饰的方法为切点等等。
- 切面(aspect):切面是通知和切点定义的结合,切面定义了在什么时候、什么位置执行什么操作(何时何地执行何种操作)
通过理解这几个概念,面向切面编程(AOP)就是要解决何时何地执行何种操作的问题。
除了以上的三个概念,AOP还有其他的概念,在这里也简单说明一下:
- 连接点:目标类中某个具体的方法(待增强);
- 织入: 织入是将切面加入的目标类的过程。在Spring AOP中,织入指的是将切面逻辑应用到目标类中并生成代理类的过程。
1.使用示例
理解清楚AOP相关的几个概念后,我们可以看一个AOP的使用示例。
创建切面,其中注解@Pointcut定义了切点信息,@Before("log")和logPrint方法定义了通知信息。
//logAOP.java
@Component
@Aspect
public class LogAOP {
//切点信息
@Pointcut("execution(* cn.test.pro.project.GsProjectService.*(..))")
public void log(){
}
//前置增强
@Before("log()")
public void logPrint(JoinPoint joinPoint){
System.out.println("---------logPrint();-------"+joinPoint.getTarget().getClass());
}
}
目标类的信息
//GsProjectService.java
@Service
public class GsProjectService {
@Autowired
private GsProjectMapper gsProjectMapper;
public String getById(String id){
return "admin";
}
}
配置文件spring-config.xml:
启动类相关信息
//Main.java
public class Main {
public static void main(String[] args){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
GsProjectService gsProjectService = (GsProjectService) ac.getBean("gsProjectService");
System.out.println(gsProjectService.getById("1"));
}
}
输出结果:
---------logPrint();-------class cn.test.pro.project.GsProjectService
cn.test.pro.project.GsProject@3370f42
2. 基本原理
说明:本文提到的AOP的基本原理是主要说明使用注解的AOP,基于XML配置的AOP类似。看本节时建议先阅读Java 动态代理机制解析
说起Spring AOP的基本原理,我们要从配置文件中配置说起:
在xml配置文件中增加如上配置后,就开启了基于注解的AOP功能。我们知道Spring 启动时会读取配置文件,并对文件中的配置项进行解析。
- 当Spring读取到该配置项后,会根据该行的命名空间AOP,查找对应的命名空间处理器AOPNamespaceHandler;
2.在AOPNamespaceHandler中,我们看到如下的代码:
public class AopNamespaceHandler extends NamespaceHandlerSupport {
public AopNamespaceHandler() {
}
public void init() {
this.registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
this.registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
}
}
在方法init()中,我们看到"aspectj-autoproxy"配置信息的解析交给了类 AspectJAutoProxyBeanDefinition进行解析。
- 现在Spring知道要使用类AspectJAutoProxyBeanDefinition进行配置解析,类AspectJAutoProxyBeanDefinition是接口BeanDefinitionParser的实现类,接着Spring调用该类的parse方法进行解析;
//AspectJAutoProxyBeanDefinition.java
public BeanDefinition parse(Element element, ParserContext parserContext) {
AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
this.extendBeanDefinition(element, parserContext);
return null;
}
- 我们特别注意parse方法中的
AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
介绍这个方法前,我们还必须要知道AOP是通过动态代理机制实现的,而类AspectJAnnotationAutoProxyCreator正是完成由目标类(target Object)到代理类的转换,可以说该类是AOP实现的核心类。
我们接着看方法registerAspectJAnnotationAutoProxyCreatorIfNecessary的功能,从方法名上我们可以看出该方法主要完成的是将AspectJAnnotationAutoProxyCreator注册到Spring容器中的功能。这样在合适的时机,Spring就可以使用该类根据目标类动态生成代理类了。
-
什么是合适的时机呢?根据动态代理机制原理(可参考Java 动态代理机制解析)的介绍,生成代理类必须需要一个实例化的目标类。
为了知道什么是合适的时机,我们还要看一下AspectJAnnotationAutoProxyCreator的类结构图,我们看到该类是接口BeanPostProcessor的实现类。
BeanPostProcessor是一种非常重要的接口,在创建Bean的过程中会调用BeanPostProcessor的postProcessAfterInitialization方法。spring的开发者也可以使用该接口的特性扩展bean的功能。而代理类的生成也正式在此处。 现在我们看一下AspectJAnnotationAutoProxyCreator的postProcessAfterInitialization方法
//方法的实现在AbstractAutoProxyCreator.java
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(bean != null) {
//如果已经生成过代理,则直接从缓存中获取
Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
if(!this.earlyProxyReferences.contains(cacheKey)) {
//生成代理对象
return this.wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
我们再看一下方法wrapIfNecessary的实现
//方法的实现在AbstractAutoProxyCreator.java
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
//如果当前已经被代理过,则直接返回;
if(beanName != null && this.targetSourcedBeans.contains(beanName)) {
return bean;
} else if(Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
} else if(!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) {
//获取切面的所有信息(包含通知和切点信息)
Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource)null);
if(specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
//根据切面信息和具体bean,创建该bean的代理类
Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
} else {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
} else {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
}
wrapIfNecessary方法主要分为两个步骤:首先找到所有切面的信息,然后根据切面信息生成代理类。
- 我们再详细看一下Spring是如何创建代理类的?
//方法的实现在AbstractAutoProxyCreator.java
protected Object createProxy(Class> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {
if(this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory)this.beanFactory, beanName, beanClass);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
if(!proxyFactory.isProxyTargetClass()) {
if(this.shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
} else {
this.evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = this.buildAdvisors(beanName, specificInterceptors);
Advisor[] var7 = advisors;
int var8 = advisors.length;
for(int var9 = 0; var9 < var8; ++var9) {
Advisor advisor = var7[var9];
proxyFactory.addAdvisor(advisor);
}
proxyFactory.setTargetSource(targetSource);
this.customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy);
if(this.advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
return proxyFactory.getProxy(this.getProxyClassLoader());
}
//方法实现在ProxyFactory.java
public Object getProxy(ClassLoader classLoader) {
return this.createAopProxy().getProxy(classLoader);
}
//方法实现在ProxyCreatorSupport.java中
protected final synchronized AopProxy createAopProxy() {
if(!this.active) {
this.activate();
}
return this.getAopProxyFactory().createAopProxy(this);
}
//方法实现在DefaultAopProxyFactory中
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if(!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
return new JdkDynamicAopProxy(config);
} else {
Class> targetClass = config.getTargetClass();
if(targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
} else {
return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass)?new ObjenesisCglibAopProxy(config):new JdkDynamicAopProxy(config));
}
}
}
经历了多个方法间的调用,我们终于看到了关注了代码。在DefaultAopProxyFactory的方法createAopProxy中,我们看到了Spring 是如何选择JDK和CGLIB两种动态代理机制的:
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP;
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP(proxy-target-class为true 或 Optimize为true即可,optimize是CGLIB中的独有配置项),但是需要保证targetClass不是接口,并且targetClass不是jdk动态代理生成的类;
- 如果目标对象没有实现接口,必须采用CGLIB库;
默认情况下,Spring会使用JDK动态代理,但是也会根据实际情况在两者之间切换。
确定使用哪种动态机制后,就可以创建目标类的代理了。至此 Spring AOP的基本原理就介绍完毕了。
3. 使用场景
了解了Spring AOP的使用示例及基本原理后,我们一块看两种Spring AOP的应用场景。
(1)增加统一日志
在第一节使用示例中,为我们展示在方法调用前增加日志打印。在Web开发中,我们可以实现Controller层或者Service统一日志打印,避免重复性日志打印代码。
(2)动态切换数据源
Spring对于多数据源有很好的支持。在Spring中,我们可以通过继承AbstractRoutingDataSource实现在程序运行时动态选择数据源。具体实现方案可以查看spring 动态切换数据源 多数据库
参考:
《Spring实战》 第三版
《Spring源码深度解析》
https://docs.spring.io/spring-framework/docs/current/javadoc-api/overview-summary.html