AspectJ 是一个基于Java语言的AOP框架,Spring 2.0以后新增了对AspectJ切点表达式支持. @AspectJ 是AspectJ 1.5新增功能,通过JDK5注解技术,允许直接在Bean类中定义切面. 新版本Spring框架,建议使用AspectJ方式来开发AOP 使用AspectJ需要导入Spring AOP和AsprctJ相关jar包 使用AspectJ 实现AOP 有两种方式: 注解方式 XML方式

基于AspectJ的注解AOP开发

要想使用AspectJ 需要先添加AspectJ依赖

  1. <!-- 引入 AspectJ 依赖-->
  2. <dependency>
  3. <groupId>org.aspectj</groupId>
  4. <artifactId>aspectjweaver</artifactId>
  5. <version>1.8.9</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework</groupId>
  9. <artifactId>spring-aspects</artifactId>
  10. <version>4.2.4.RELEASE</version>
  11. </dependency>

然后在applicationContext.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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 开启 AspectJ的注解开发 自动代理-->
    <aop:aspectj-autoproxy/>

    <!-- 配置bean 也可以用Spring ioc的注解方式去实现  -->
    <bean id="productDao" class="com.prim.aspectj.ProductDao"/>

    <!-- 配置切面类 -->
    <bean class="com.prim.aspectj.MyAspectAnno"/>
</beans>

定义切面类使用@Aspect注解

/**
 * 切面类
 * 注意将切面定义到applicationContext
 *
 * @author prim
 */
@Aspect
public class MyAspectAnno {}

@AspectJ 提供不同的通知类型

  • @Before 前置通知,相当于BeforeAdvice
  • @AfterReturning 后置通知,相当于AfterReturningAdvice
  • @Around 环绕通知,相当于MethodInterceptor
  • @AfterThrowing 异常抛出通知,相当于ThrowAdvice
  • @After 最终final通知,不管是否异常,该通知都会执行
  • @DeclareParents 引介通知,相当于IntroductionInterceptor(可以先不用掌握)

在通知中通过value属性定义切点

  • 通过execution函数,可以定义切点的方法切入
  • 语法:
    • execution(<访问修饰符>?<返回类型><方法名>(<参数>)<异常>)
    • 例如:匹配所有类:excution(public (..)) 任意返回类型 任意方法 任意参数 只要是public方法都会执行
    • 匹配指定包下所有类方法 execution( com.imooc.dao.(..)) 不包含子包
    • elecution( com.imooc.dao..(..)) ..* 表示包、子孙包下所有类
    • 匹配指定类所有方法 execution( com.imooc.service.UserService.(..))
    • 匹配实现特定接口所有类方法 execution( com.imooc.dao.GenericDAO+.(..)) GenericDAO+ 实现表示接口或抽象类GenericDAO 的所有类
    • 匹配所有save开头的方法 execution( save(..))

为目标类,定义切面类. 通过例子来进行理解

通过@Aspect 来定义切面类,然后在切面类的方法加入通知注解.

前置通知 @Before

  • 可以在方法中传入JoinPoint对象,用来获得切点信息.
    /**
     * 前置通知
     * value 定义切入点:在save的时候进行校验
     * JoinPoint 获得切点信息
     */
    @Before(value = "execution(* com.prim.aspectj.ProductDao.save(..))")
    public void before(JoinPoint joinPoint) {
        System.out.println("MyAspectAnno.before 权限校验 ===================" + joinPoint);
    }

后置通知 @AfterReturning ,可以通过returning属性得到切入方法的返回值

    /**
     * 后置通知
     * returning 获得切入方法的返回值
     */
    @AfterReturning(value = "execution(* com.prim.aspectj.ProductDao.update(..))", returning = "result")
    public void afterReturning(Object result) {
        System.out.println("MyAspectAnno.afterReturning 后置通知 ===================" + result);
    }

环绕通知 @Around

  • around方法的返回值就是目标代理方法执行返回值
  • 参数为ProceedingJoinPoint可以调用拦截目标方法执行

    如果不调用ProceedingJoinPoint的proceed方法,那么目标方法就被拦截了

  /**
     * 环绕通知
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = "execution(* com.prim.aspectj.ProductDao.delete(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("MyAspectAnno.around 环绕前通知=======================");
        //执行目标方法 如果不调用目标方法就会不执行了
        Object proceed = joinPoint.proceed();
        System.out.println("MyAspectAnno.around 环绕后通知======================");
        return proceed;
    }

异常抛出通知 @AfterThrowing

  • 通过设置throwing属性,可以设置发生异常对象参数
    @AfterThrowing(value = "execution(* com.prim.aspectj.ProductDao.findAll(..))", throwing = "e")
    public void afterThrowing(Throwable e) {
        System.out.println("MyAspectAnno.afterThrowing 异常抛出通知" + e.getMessage());
    }

最终通知@After

  • 无论是否出现异常,最终通知总是会被执行
    @After(value = "execution(* com.prim.aspectj.ProductDao.findOne(..))")
    public void after() {
        System.out.println("MyAspectAnno.after 最终通知===============================");
    }

下面测试一下,上述写的几个通知

    @Resource(name = "productDao")
    private ProductDao productDao;

    @Test
    public void test1() {
        productDao.save();
        productDao.delete();
        productDao.update();
        productDao.findAll();
        productDao.findOne();
    }

运行结果如下:
image.png
在上述的使用过程中,大家有没有觉得很麻烦,比如save()增强前置通知,突然有个需求需要对save方法要求增强后置通知,那我们还得需要重新写一个方法标记后置通知,一旦多起来非常难以维护.通过@Pointcut 为切点命名,统一进行管理

  • 每个通知内定义切点,会造成工作量大,不易维护,对于重复的切点,可以使用@Pointcut进行定义
  • 切点方法:private void 无参数方法,方法名为切点名
  • 但通知多个切点时,可以通过||连接

修改如下,我们可以将定义好的切点随意放到定义好的通知方法中,便于管理

/**
     * 前置通知
     * value 定义切入点:在save的时候进行校验
     * JoinPoint 获得切点信息
     */
    @Before(value = "myPointcut1()||myPointcut2()")
    public void before(JoinPoint joinPoint) {
        System.out.println("MyAspectAnno.before 权限校验 ===================" + joinPoint);
    }

    /**
     * 后置通知
     * returning 获得切入方法的返回值
     */
    @AfterReturning(value = "myPointcut2()", returning = "result")
    public void afterReturning(Object result) {
        System.out.println("MyAspectAnno.afterReturning 后置通知 ===================" + result);
    }

    @Around(value = "myPointcut3()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("MyAspectAnno.around 环绕前通知=======================");
        //执行目标方法 如果不调用目标方法就会不执行了
        Object proceed = joinPoint.proceed();
        System.out.println("MyAspectAnno.around 环绕后通知======================");
        return proceed;
    }

    @AfterThrowing(value = "myPointcut4()", throwing = "e")
    public void afterThrowing(Throwable e) {
        System.out.println("MyAspectAnno.afterThrowing 异常抛出通知" + e.getMessage());
    }

    @After(value = "myPointcut5()")
    public void after() {
        System.out.println("MyAspectAnno.after 最终通知===============================");
    }

    /**
     * 定义一个切点 方便维护
     */
    @Pointcut(value = "execution(* com.prim.aspectj.ProductDao.save(..))")
    private void myPointcut1() {
    }

    @Pointcut(value = "execution(* com.prim.aspectj.ProductDao.update(..))")
    private void myPointcut2() {
    }

    @Pointcut(value = "execution(* com.prim.aspectj.ProductDao.delete(..))")
    private void myPointcut3() {
    }

    @Pointcut(value = "execution(* com.prim.aspectj.ProductDao.findAll(..))")
    private void myPointcut4() {
    }

    @Pointcut(value = "execution(* com.prim.aspectj.ProductDao.findOne(..))")
    private void myPointcut5() {
    }

image.png

基于AspectJ的XML方式的AOP

使用xml配置切面

  • 编写切面类
/**
 * 定义切面类 配置到配置文件中
 */
public class MyAspectXml {
    /**
     * 前置通知
     */
    public void before(JoinPoint joinPoint) {
        System.out.println("MyAspectXml.before XML方式 前置通知================" + joinPoint);
    }

    /**
     * 后置通知
     */
    public void afterReturning(Object result) {
        System.out.println("MyAspectXml.afterReturning XML方式 后置通知====================" + result);
    }

    /**
     * 环绕通知
     */
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("MyAspectXml.around XML方式环绕通知之前================");
        Object proceed = joinPoint.proceed();
        System.out.println("MyAspectXml.around XML方式环绕通知之后================");
        return proceed;
    }

    public void afterThrowing(Throwable e) {
        System.out.println("MyAspectXml.afterThrowing XML方式异常抛出通知:" + e.getMessage());
    }

    public void after(){
        System.out.println("MyAspectXml.after XML方式最终通知");
    }
}
  • xml切面类配置applicationContext.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 https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- XML配置方式完成AOP开发 -->

    <bean id="customerDao" class="com.prim.aspectj.xml.CustomerDaoImpl"/>

    <!-- 配置切面类 -->
    <bean id="myAspectXml" class="com.prim.aspectj.xml.MyAspectXml"/>
</bean>
  • 配置aop增强

<!-- aop 的配置 -->
    <aop:config>
        <!-- 配置切入点 -->
        <aop:pointcut expression="execution(* com.prim.aspectj.xml.CustomerDao.save(..))" id="pointcut1"/>
        <aop:pointcut expression="execution(* com.prim.aspectj.xml.CustomerDao.update(..))" id="pointcut2"/>
        <aop:pointcut expression="execution(* com.prim.aspectj.xml.CustomerDao.delete(..))" id="pointcut3"/>
        <aop:pointcut expression="execution(* com.prim.aspectj.xml.CustomerDao.findOne(..))" id="pointcut4"/>
        <aop:pointcut expression="execution(* com.prim.aspectj.xml.CustomerDao.findAll(..))" id="pointcut5"/>
        <!-- 告知哪个类是切面 配置切面类 -->
        <aop:aspect ref="myAspectXml">
            <!-- 配置切入点的通知 -->
            <!-- 前置通知 method 设置切入类的方法  前置通知应用到的切入点 pointcut-ref -->
            <aop:before method="before" pointcut-ref="pointcut1"/>
            <aop:after-returning method="afterReturning" pointcut-ref="pointcut2" returning="result"/>
            <aop:around method="around" pointcut-ref="pointcut3"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut4" throwing="e"/>
            <aop:after method="after" pointcut-ref="pointcut5"/>

            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut3" throwing="e"/>
            <!-- 前置通知添加其他的切入点 -->
            <aop:before method="before" pointcut-ref="pointcut2"/>
        </aop:aspect>
    </aop:config>
    @Resource(name = "customerDao")
    public CustomerDao customerDao;

    @Test
    public void test() {
        customerDao.save();
        customerDao.update();
        customerDao.delete();
        customerDao.findOne();
        customerDao.findAll();
    }

运行结果:
image.png

可以看出,XML方式和注解的方式没有多大的区别,XML的方式便于管理和维护、注解的方式便于开发.

这两种开发方式,在实际开发中都会使用基于AspectJ的AOP的开发方式,而很少会使用Spring 传统AOP的方式. 在使用AspectJ AOP 可能有的企业会用到注解的方式,也可能会用到xml的开发方式.