Spring基础(AOP)

1.AOP 简介

AOP : AspectOrientedProgramming的缩写,意思时面向切面编程。可以通过预编译和运行期间动态代理实现在不修改源代码的情况下,给程序统一添加功能的一种技术。 我们现在做的一些非业务,如: 日志、事物、安全等都会写在业务代码中,但这些代码往往时重复的,AOP 就实现了把这些业务需求与系统需求分开,这种解决方式页称作 代理机制

原理:
aop 底层将采用代理机制进行实现

  • 接口+ 实现类: spring采用jdk 的动态代理 Proxy
  • 实现类: spring 默认采用 cglib 字节码增强。

基本概念:

  1. target: 目标类,需要被代理的类。

  2. JoinPoint : 连接点,指哪些可能被拦截的方法们。例如: UserService 中的 m1(),m2(),m3()

  3. pointcut: 切入点,已经被增强的连接点,PointCut 数据 JoinCut,

  4. advice: 通知/增强 ,增强代码

    • 前置通知:(before),在方法执行前执行,如果通知出现异常,阻止方法执行。用于各种校验
    • 后置通知: (after),方法执行结束后执行,无论方法中是否出现异常,多用于清理现场。
    • 后置通知:(afterReturning),方法正常返回后执行,如果方法中抛出异常,通知无法执行必须在方法执行后才执行。所以可以获得方法的返回值。多用于常规数据处理。
    • 环绕通知: (around),方法执行前后分别执行,可以阻止方法的执行,必须手动执行目标方法。十分强大、可以做任何事情。
    • 引介通知 :org.springframework.aop.IntroductionInterceptor:在目标类中添加一些新的方法和属性
  5. Aspect: 切面,切入点(pointcut)+ 通知(advice)=指定切点的非业务逻辑类。例如:LogAspect

  6. Weaving:织入,是指把 advice 应用到 target 来创建代理对象的过程。

  7. Proxy:代理类。在 Spring AOP 中有两种代理方式,JDK动态代理和 CGLib代理。默认情况下,TargetObject 实现了接口时,则采用 JDK动态代理,例如,AServiceImpl;反之,采用CGLib代理,例如,BServiceImpl。强制使用CGLib代理需要将 < aop:config >的 proxy-target-class属性设为true。

2.AOP编程

使用springAOP 编程可以基于两种方式,注解和xml配置方式。

2.1 基于注解

第一步:在 xml文件中声明激活自动扫描组件功能,同时激活自动代理功能

<?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" xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop 
        	http://www.springframework.org/schema/aop/spring-aop.xsd">
	
    <!-- 扫描com.yzh包下的所有bean,交给IOC容器管理 -->
    <context:component-scan base-package="com.yzh"/>
    <!-- 开启主机 -->
    <context:annotation-config />
    <!-- 添加aop注解驱动(AspectJ) -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

第二步:为Aspect切面类添加注解

// 声明这是一个组件
@Component
// 声明这是一个切面Bean,AnnotaionAspect是一个面,由框架实现的
@Aspect
public class AnnotaionAspect {

   private final static Logger log = Logger.getLogger(AnnotaionAspect.class);
   
   // 配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点切点的集合,
   // 这个表达式所描述的是一个虚拟面(规则),就是为了Annotation扫描时能够拿到注解中的内容
   @Pointcut("execution(* com.yzh.aop.service..*(..))")
   public void aspect(){}
   
   /*
    * 配置前置通知,使用在方法aspect()上注册的切入点
    * 同时接受JoinPoint切入点对象,可以没有该参数
    */
   @Before("aspect()")
   public void before(JoinPoint joinPoint){
      log.info("before " + joinPoint);
   }
   
   // 配置后置通知,使用在方法aspect()上注册的切入点
   @After("aspect()")
   public void after(JoinPoint joinPoint){
      log.info("after " + joinPoint);
   }
   
   // 配置环绕通知,使用在方法aspect()上注册的切入点
   @Around("aspect()")
   public void around(JoinPoint joinPoint){
      long start = System.currentTimeMillis();
      try {
         ((ProceedingJoinPoint) joinPoint).proceed();
         long end = System.currentTimeMillis();
         log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
      } catch (Throwable e) {
         long end = System.currentTimeMillis();
         log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage());
      }
   }
   
   // 配置后置返回通知,使用在方法aspect()上注册的切入点
   @AfterReturning("aspect()")
   public void afterReturn(JoinPoint joinPoint){
      log.info("afterReturn " + joinPoint);
   }
   
   // 配置抛出异常后通知,使用在方法aspect()上注册的切入点
   @AfterThrowing(pointcut="aspect()", throwing="ex")
   public void afterThrow(JoinPoint joinPoint, Exception ex){
      log.info("afterThrow " + joinPoint + "\t" + ex.getMessage());
   }
   
}

测试代码:正常调用相应代码

@ContextConfiguration(locations = {"classpath*:application-context.xml"}) // 把配置文件加载进来
@RunWith(SpringJUnit4ClassRunner.class)
public class AnnotationTest {
	@Autowired
	MemberService memberService;
	
	@Test
	public void test(){
		System.out.println("=====这是一条华丽的分割线======");
		
		memberService.save(new Member());

		System.out.println("=====这是一条华丽的分割线======");
		try {
			memberService.delete(1L);
		} catch (Exception e) {
			// e.printStackTrace();
		}
	}
}

MemberService 代码如下:

@Service
public class MemberService {

   private final static Logger log = Logger.getLogger(AnnotaionAspect.class);
   
   public Member get(long id){
      log.info("getMemberById method . . .");
      return new Member();
   }
   
   
   public Member get(){
      log.info("getMember method . . .");
      return new Member();
   }
   
   public void save(Member member){
      log.info("save member method . . .");
   }
   
   public boolean delete(long id) throws Exception{
      log.info("delete method . . .");
      throw new Exception("spring aop ThrowAdvice演示");
   }
   
}

对于web开发者,spring 有个很好的建议,就是定义一个SystemArchitecture

@Aspect
public class SystemArchitecture {

    // web 层
    @Pointcut("within(com.javadoop.web..*)")
    public void inWebLayer() {}

    // service 层
    @Pointcut("within(com.javadoop.service..*)")
    public void inServiceLayer() {}

    // dao 层
    @Pointcut("within(com.javadoop.dao..*)")
    public void inDataAccessLayer() {}

    // service 实现,注意这里指的是方法实现,其实通常也可以使用 bean(*ServiceImpl)
    @Pointcut("execution(* com.javadoop..service.*.*(..))")
    public void businessService() {}

    // dao 实现
    @Pointcut("execution(* com.javadoop.dao.*.*(..))")
    public void dataAccessOperation() {}
}

上面这个 SystemArchitecture 定义了一堆PointCut ,随后在任何需要PointCut 的地方都可以直接引用。

@Aspect
public class AdviceExample {

    // 这里会用到我们前面说的 SystemArchitecture
    // 下面方法就是写拦截 "dao层实现"
    @Before("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ... 实现代码
    }

    // 当然,我们也可以直接"内联"Pointcut,直接在这里定义 Pointcut
    // 把 Advice 和 Pointcut 合在一起了,但是这两个概念我们还是要区分清楚的
    @Before("execution(* com.javadoop.dao.*.*(..))")
    public void doAccessCheck() {
        // ... 实现代码
    }

    @AfterReturning("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

    @AfterReturning(
        pointcut="com.javadoop.aop.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // 这样,进来这个方法的处理时候,retVal 就是相应方法的返回值,是不是非常方便
        //  ... 实现代码
    }

    // 异常返回
    @AfterThrowing("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ... 实现代码
    }

    @AfterThrowing(
        pointcut="com.javadoop.aop.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ... 实现代码
    }

    // 注意理解它和 @AfterReturning 之间的区别,这里会拦截正常返回和异常的情况
    @After("com.javadoop.aop.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // 通常就像 finally 块一样使用,用来释放资源。
        // 无论正常返回还是异常退出,都会被拦截到
    }

    // 感觉这个很有用吧,既能做 @Before 的事情,也可以做 @AfterReturning 的事情
    @Around("com.javadoop.aop.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}

2.2 xml 配置

xml配置方式,其实也一样简单,也是分为两步:

<!-- 将切面Bean交给IOC容器 -->
<bean id="xmlAspect" class="com.yzh.aop.aspect.XmlAspect"></bean>
<!-- AOP配置-->
<aop:config>
   <!--声明一个切面,并注入切面Bean,相当于@Aspect -->
   <aop:aspect ref="xmlAspect" >
      <!--配置一个切入点,相当于@Pointcut -->
      <aop:pointcut expression="execution(* com.yzh.aop.service..*(..))" id="simplePointcut"/>
      <!--配置通知,相当于@Before、@After、@AfterReturn、@Around、@AfterThrowing -->
      <aop:before pointcut-ref="simplePointcut" method="before"/>
      <aop:after pointcut-ref="simplePointcut" method="after"/>
      <aop:after-returning pointcut-ref="simplePointcut" method="afterReturn"/>
      <aop:after-throwing pointcut-ref="simplePointcut" method="afterThrow" throwing="ex"/>
      <aop:around pointcut-ref="simplePointcut"  method="around"/>
   </aop:aspect>
</aop:config>
/**
 * XML版Aspect切面Bean(理解为TrsactionManager)
 */
public class XmlAspect {

   private final static Logger log = Logger.getLogger(XmlAspect.class);
   
   /*
    * 配置前置通知,使用在方法aspect()上注册的切入点
    * 同时接受JoinPoint切入点对象,可以没有该参数
    */
   public void before(JoinPoint joinPoint){
//    System.out.println(joinPoint.getArgs()); //获取实参列表
//    System.out.println(joinPoint.getKind());   //连接点类型,如method-execution
//    System.out.println(joinPoint.getSignature()); //获取被调用的切点
//    System.out.println(joinPoint.getTarget()); //获取目标对象
//    System.out.println(joinPoint.getThis());   //获取this的值
      
      log.info("before " + joinPoint);
   }
   
   // 配置后置通知,使用在方法aspect()上注册的切入点
   public void after(JoinPoint joinPoint){
      log.info("after " + joinPoint);
   }
   
   // 配置环绕通知,使用在方法aspect()上注册的切入点
   public void around(JoinPoint joinPoint){
      long start = System.currentTimeMillis();
      try {
         ((ProceedingJoinPoint) joinPoint).proceed();
         long end = System.currentTimeMillis();
         log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
      } catch (Throwable e) {
         long end = System.currentTimeMillis();
         log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage());
      }
   }
   
   // 配置后置返回通知,使用在方法aspect()上注册的切入点
   public void afterReturn(JoinPoint joinPoint){
      log.info("afterReturn " + joinPoint);
   }
   
   // 配置抛出异常后通知,使用在方法aspect()上注册的切入点
   public void afterThrow(JoinPoint joinPoint, Exception ex){
      log.info("afterThrow " + joinPoint + "\t" + ex.getMessage());
   }
   
}

2.3 切入点表达式规则(execution)

execution表达式格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?
  • modifiers-pattern:方法的操作权限
  • ret-type-pattern:返回值
  • declaring-type-pattern:方法所在的包
  • name-pattern:方法名
  • parm-pattern:参数名
  • throws-pattern:异常

其中,除 ret-type-pattern 和 name-pattern 之外,其他都是可选的。上例中,execution(* com.yzh.aop.service…*(…))表示com.yzh.aop.service 包下,返回值为任意类型;方法名任意;参数不作限制的所有方法。

最后说一下通知参数,可以通过args来绑定参数,这样就可以在通知(Advice)中访问具体参数了。例如,aop:aspect配置如下:

<aop:config> 
	<aop:aspect ref="xmlAspect">
	 <aop:pointcut id="simplePointcut" expression="execution(* com.my.aop.service..*(..)) and args(msg,..)" /> 
	 <aop:after pointcut-ref="simplePointcut" Method="after"/>
	</aop:aspect>
</aop:config> 

上面的代码 args(msg,…)是指将切入点方法上的第一个 String 类型参数添加到参数名为 msg 的通知的入参上,这样就可以直接使用该参数啦。

在上面的Aspect切面Bean中已经看到了,每个通知方法第一个参数都是 JoinPoint。其实,在Spring中,任何通知(Advice)方法都可以将第一个参数定义为 org.aspectj.lang.JoinPoint 类型用以接受当前连接点对象。JoinPoint 接口提供了一系列有用的方法, 比如 getArgs() (返回方法参数)、getThis() (返回代理对象)、getTarget() (返回目标)、getSignature() (返回正在被通知的方法相关信息)和 toString() (打印出正在被通知的方法的有用信息)。

2.4 JDK动态代理

spring 动态代理是spring IOC 容器在调用getBean()时,返回的是代理类的实例,而这个代理类的实例 是Spring 采用JDK Proxy 或CGLIB 动态生成的。

getBean() 方法 是用于查找或实例化容器中的bean,这也是为什么Spring AOP 只能怪作用域spring 容器中bean 的原因。

Spring 管理Bean 的生命周期很复杂,要在getBean 的时候返回 代理类实例,需要理解Bean 的生命周期中的扩展点即生命周期中的回调方法。

2.4.1 Bean 生命周期回调方法

  • InstantiationAwareBeanPostProcessor
  • BeanPostProcessor
    Spring基础(AOP)
  • InstantiationAwareBeanPostProcessor作用于实例化阶段的前后
  • BeanPostProcessor作用于初始化阶段的前后

InstantiationAwareBeanPostProcessor实际上继承了BeanPostProcessor接口,严格意义上来看他们不是两兄弟,而是两父子。但是从生命周期角度我们重点关注其特有的对实例化阶段的影响,图中省略了从BeanPostProcessor继承的方法。

BeanPostProcessor 后置处理器的调用发生在 Spring IOC 容器完成对Bean实例对象的创建和属性的依赖注入完成之后。
BeanPostProcessor是一个接口,其初始化前的操作方法和初始化后的操作方法均委托其实现子类来实现,在Spring中,BeanPostProcessor的实现子类非常的多,分别完成不同的操作。

AbstractAutoProxyCreator 是 BeanPostProcessor 的子类,该类重写了 postProcessAfterInitialization()方法。
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
   if (bean != null) {
      Object cacheKey = getCacheKey(bean.getClass(), beanName);
      if (!this.earlyProxyReferences.contains(cacheKey)) {
         return wrapIfNecessary(bean, beanName, cacheKey);
      }
   }
   return bean;
}
 

wrapIfNecessary()

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
   if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
      return bean;
   }
   // 判断是否不应该代理这个bean
   if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
      return bean;
   }
   /*
   	* 判断是否是一些InfrastructureClass或者是否应该跳过这个bean。 
   	* 所谓InfrastructureClass就是指Advice/PointCut/Advisor等接口的实现类。 
   	* shouldSkip默认实现为返回false,由于是protected方法,子类可以覆盖。
    */ 
   if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
      this.advisedBeans.put(cacheKey, Boolean.FALSE);
      return bean;
   }

   // 获取这个bean的advice 
   // Create proxy if we have advice.
   Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
   if (specificInterceptors != DO_NOT_PROXY) {
      this.advisedBeans.put(cacheKey, Boolean.TRUE);
      // 创建代理
      Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
      this.proxyTypes.put(cacheKey, proxy.getClass());
      return proxy;
   }

   this.advisedBeans.put(cacheKey, Boolean.FALSE);
   return bean;
}

createProxy()

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
      @Nullable 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 (shouldProxyTargetClass(beanClass, beanName)) {
         proxyFactory.setProxyTargetClass(true);
      }
      else {
         evaluateProxyInterfaces(beanClass, proxyFactory);
      }
   }

   Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
   proxyFactory.addAdvisors(advisors);
   proxyFactory.setTargetSource(targetSource);
   customizeProxyFactory(proxyFactory);

   proxyFactory.setFrozen(this.freezeProxy);
   if (advisorsPreFiltered()) {
      proxyFactory.setPreFiltered(true);
   }

   return proxyFactory.getProxy(getProxyClassLoader());
}

整个过程下来,我们发现调用的是 proxyFactory.getProxy()方法。proxyFactory 有JDK和CGLib的,那么我们该如何选择呢?最终调用的是DefaultAopProxyFactory的createAopProxy()方法:

createAopProxy()

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

   @Override
   public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
      if (config.isOptimize() || config.isProxyTargetClass() || 
          hasNoUserSuppliedProxyInterfaces(config)) {
         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.");
         }
         if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
         }
         return new ObjenesisCglibAopProxy(config);
      }
      else {
         return new JdkDynamicAopProxy(config);
      }
   }

}

hasNoUserSuppliedProxyInterfaces()

  /**
    * Determine whether the supplied {@link AdvisedSupport} has only the
    * {@link org.springframework.aop.SpringProxy} interface specified
    * (or no proxy interfaces specified at all).
    */
   private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
      Class<?>[] ifcs = config.getProxiedInterfaces();
      return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
   }

我们知道 InvocationHandler 是 JDK 动态代理的核心,生成的代理对象的方法调用都会委托到 InvocationHandler.invoke()方法。而从 JdkDynamicAopProxy 的源码我们可以看到这个类其实也实现了InvocationHandler。

2.4.2 JDK 动态代理 操作步骤

第一步: 接口+ 实现类

package com.mikan.proxy; 
/**
 * @author 
 * @date 2015-09-15 18:00
 */
public interface HelloWorld { 
    void sayHello(String name); 
}
package com.mikan.proxy; 
/**
 * @author 
 * @date 2015-09-15 18:01
 */
public class HelloWorldImpl implements HelloWorld {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
}

第二步: 实现 InvocationHandler 接口

package com.mikan.proxy; 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
 
/**
 * @author 
 * @date 2015-09-15 19:53
 */
public class CustomInvocationHandler implements InvocationHandler {
    private Object target; 
    public CustomInvocationHandler(Object target) {
        this.target = target;
    } 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invocation");
        Object retVal = method.invoke(target, args);
        System.out.println("After invocation");
        return retVal;
    }
}

第三步 : 使用代理

package com.mikan.proxy;
 
import java.lang.reflect.Proxy;
 
/**
 * @author 
 * @date 2015-09-15 18:01
 */
public class ProxyTest { 
    public static void main(String[] args) throws Exception {
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
 
        CustomInvocationHandler handler = new CustomInvocationHandler(new HelloWorldImpl());
        HelloWorld proxy = (HelloWorld) Proxy.newProxyInstance(
                ProxyTest.class.getClassLoader(),
                new Class[]{HelloWorld.class},
                handler);
        proxy.sayHello("Mikan");
    }
 
}
上一篇:spring aop事件两种实现方式,带注解和不带注解


下一篇:如何在Windows中为超过768M的Android模拟器分配内存?