Spring AOP的使用

spring虽然有自己的AOP,但使用起来太复杂(需要自己实现通知接口、代理类等),就不介绍了,这里介绍spring-aop + aspect的方式

一、添加依赖(maven)

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>4.3.12.RELEASE</version>
</dependency>

<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.8.13</version>
</dependency>
  • spring-aop包中定义了相应的接口
  • aspectjweaver包中提供了相应的注解

二、基于注解的Spring AOP开发

1. 定义目标类接口和实现类

/**
 * 接口类
 */
public interface UserDao {
    int addUser();
}

/**
* 实现类
*/ @Repository public class UserDaoImp implements UserDao { @Override public int addUser() { System.out.println("add user ......"); return 6666; } } 

2. 编写Spring AOP的aspect 类

@Aspect
@Component
public class MyAspect {

    /**
     * 前置通知
     */
    @Before("execution(* com.wslook.aspect.UserDao.addUser(..))")
    public void before(){
        System.out.println("前置通知....");
    }

    /**
     * 后置通知
     * returnVal,切点方法执行后的返回值
     */
    @AfterReturning(value="execution(* com.wslook.aspect.UserDao.addUser(..))",returning = "returnVal")
    public void AfterReturning(Object returnVal){
        System.out.println("后置通知...."+returnVal);
    }


    /**
     * 环绕通知
     * @param joinPoint 可用于执行切点的类
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.wslook.aspect.UserDao.addUser(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知前....");
        Object obj= (Object) joinPoint.proceed();
        System.out.println("环绕通知后....");
        return obj;
    }

    /**
     * 抛出异常通知
     * @param e
     */
    @AfterThrowing(value="execution(* com.wslook.aspect.UserDao.addUser(..))",throwing = "e")
    public void afterThrowable(Throwable e){
        System.out.println("出现异常:msg="+e.getMessage());
    }

    /**
     * 无论什么情况下都会执行的方法
     */
    @After(value="execution(* com.wslook.aspect.UserDao.addUser(..))")
    public void after(){
        System.out.println("最终通知....");
    }
}

3. 编写配置文件

<?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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">


    <context:component-scan base-package="com.wslook.aspect"/>

    <!-- 启动@aspectj的自动代理支持-->
    <aop:aspectj-autoproxy />

</beans>

4. 编写测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations= "classpath:spring/spring-aspectj.xml")
public class UserDaoAspectJ {
    @Autowired
    UserDao userDao;

    @Test
    public void aspectJTest(){
        userDao.addUser();
    }
} 

运行结果:

环绕通知前....
前置通知....
add user ......
环绕通知后....
最终通知....
后置通知....6666

二、基于XML的开发

1. 定义一个切面类

public class MyAspectXML {

    public void before(){
        System.out.println("MyAspectXML====前置通知");
    }

    public void afterReturn(Object returnVal){
        System.out.println("后置通知-->返回值:"+returnVal);
    }

    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("MyAspectXML=====环绕通知前");
        Object object= joinPoint.proceed();
        System.out.println("MyAspectXML=====环绕通知后");
        return object;
    }

    public void afterThrowing(Throwable throwable){
        System.out.println("MyAspectXML======异常通知:"+ throwable.getMessage());
    }

    public void after(){
        System.out.println("MyAspectXML=====最终通知..来了");
    }
} 

2. 编写配置文件(spring-aspectj.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-3.1.xsd
           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 定义目标对象 -->
    <bean name="productDao" class="com.wslook.aspect.UserDaoImp"/>

    <!-- 定义切面 -->
    <bean name="myAspectXML" class="com.wslook.aspect.MyAspectXML"/>
    <!-- 配置AOP 切面 -->
    <aop:config>
        <!-- 定义切点函数 -->
        <aop:pointcut id="pointcut" expression="execution(* com.wslook.aspect.UserDao.addUser(..))"/>

        <!-- 定义通知 order 定义优先级,值越小优先级越大-->
        <aop:aspect ref="myAspectXML" order="0">
            <!-- 定义通知
            method 指定通知方法名,必须与MyAspectXML中的相同
            pointcut 指定切点函数
            -->
            <aop:before method="before" pointcut-ref="pointcut"/>

            <!-- 后置通知  returning="returnVal" 定义返回值 必须与类中声明的名称一样-->
            <aop:after-returning method="afterReturn" pointcut-ref="pointcut" returning="returnVal"/>

            <!-- 环绕通知 -->
            <aop:around method="around" pointcut-ref="pointcut"/>

            <!--异常通知 throwing="throwable" 指定异常通知错误信息变量,必须与类中声明的名称一样-->
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="throwable"/>

            <!--
                 method : 通知的方法(最终通知)
                 pointcut-ref : 通知应用到的切点方法
                -->
            <aop:after method="after" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

3. 测试结果:

MyAspectXML====前置通知
MyAspectXML=====环绕通知前
add user ......
MyAspectXML=====最终通知..来了
MyAspectXML=====环绕通知后
后置通知-->返回值:6666

三、开发技巧

1. 定义切入点函数

/**
 * 使用Pointcut定义切点
 */
@Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")
private void myPointcut(){}

/**
 * 应用切入点函数
 */
@After(value="myPointcut()")
public void afterDemo(){
    System.out.println("最终通知....");
} 

2. Aspect优先级

  • 在同一个切面中定义多个通知响应同一个切点函数,执行顺序为声明顺序:
  • 如果在不同的切面中定义多个通知响应同一个切点,进入时,优先级高的切面类中的通知函数优先执行,退出时则最后执行
  • Ordered 接口用于控制切面类的优先级,重写getOrder方法,定制返回值,返回值(int 类型)越小优先级越大。
@Aspect
public class AspectOne implements Ordered {

    /**
     * Pointcut定义切点函数
     */
    @Pointcut("execution(* com.zejian.spring.springAop.dao.UserDao.deleteUser(..))")
    private void myPointcut(){}

    @Before("myPointcut()")
    public void beforeOne(){
        System.out.println("前置通知..AspectOne..执行顺序1");
    }

    @Before("myPointcut()")
    public void beforeTwo(){
        System.out.println("前置通知..AspectOne..执行顺序2");
    }

    @AfterReturning(value = "myPointcut()")
    public void AfterReturningThree(){
        System.out.println("后置通知..AspectOne..执行顺序3");
    }

    @AfterReturning(value = "myPointcut()")
    public void AfterReturningFour(){
        System.out.println("后置通知..AspectOne..执行顺序4");
    }

    /**
     * 定义优先级,值越低,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

四、切入点指示符

1. 通配符

在定义匹配表达式时,通配符几乎随处可见,如*、.. 、+ ,它们的含义如下:

  • .. :匹配方法定义中的任意数量的参数,或任意数量包

    //任意返回值,任意名称,任意参数的公共方法
    execution(public * *(..))
    //匹配com.zejian.dao包及其子包中所有类中的所有方法
    within(com.zejian.dao..*) 
  • + :匹配给定类的任意子类

    //匹配实现了DaoUser接口的所有子类的方法
    within(com.zejian.dao.DaoUser+) 
  • * :匹配任意数量的字符

    //匹配com.zejian.service包及其子包中所有类的所有方法
    within(com.zejian.service..*)
    //匹配以set开头,参数为int类型,任意返回值的方法
    execution(* set*(int)) 

2. 类型签名表达式

为了方便通过类型(如接口、类名、包名)过滤方法,Spring AOP 提供了within关键字。其语法格式如下:

within(<type name>) 

type name 则使用包名或者类名替换即可,来点案例吧。

//匹配com.zejian.dao包及其子包中所有类中的所有方法
@Pointcut("within(com.zejian.dao..*)")

//匹配UserDaoImpl类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl)")

//匹配UserDaoImpl类及其子类中所有方法
@Pointcut("within(com.zejian.dao.UserDaoImpl+)")

//匹配所有实现UserDao接口的类的所有方法
@Pointcut("within(com.zejian.dao.UserDao+)") 

3. 方法签名表达式

如果想根据方法签名进行过滤,关键字execution可以帮到我们,语法表达式如下

//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters)) 

对于给定的作用域、返回值类型、完全限定类名以及参数匹配的方法将会应用切点函数指定的通知,这里给出模型案例:

//匹配UserDaoImpl类中的所有方法
@Pointcut("execution(* com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中的所有公共方法并且返回值为int类型
@Pointcut("execution(public int com.zejian.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl类中第一个参数为int类型的所有公共的方法
@Pointcut("execution(public * com.zejian.dao.UserDaoImpl.*(int , ..))") 

4. 其他指示符

  • bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;

    //匹配名称中带有后缀Service的Bean。
    @Pointcut("bean(*Service)")
    private void myPointcut1(){} 
  • this :用于匹配当前AOP代理对象类型的执行方法;请注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配

    //匹配了任意实现了UserDao接口的代理对象的方法进行过滤
    @Pointcut("this(com.zejian.spring.springAop.dao.UserDao)")
    private void myPointcut2(){} 
  • target :用于匹配当前目标对象类型的执行方法;

    //匹配了任意实现了UserDao接口的目标对象的方法进行过滤
    @Pointcut("target(com.zejian.spring.springAop.dao.UserDao)")
    private void myPointcut3(){} 
  • @within:用于匹配所以持有指定注解类型内的方法;请注意与within是有区别的, within是用于匹配指定类型内的方法执行;

    //匹配使用了MarkerAnnotation注解的类(注意是类)
    @Pointcut("@within(com.zejian.spring.annotation.MarkerAnnotation)")
    private void myPointcut4(){} 
  • @annotation(com.zejian.spring.MarkerMethodAnnotation) : 根据所应用的注解进行方法过滤

    //匹配使用了MarkerAnnotation注解的方法(注意是方法)
    @Pointcut("@annotation(com.zejian.spring.annotation.MarkerAnnotation)")
    private void myPointcut5(){} 

ok~,关于表达式指示符就介绍到这,我们主要关心前面几个常用的即可,不常用过印象即可。这里最后说明一点,切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或者&&、||、!),如下一个简单例子:

//匹配了任意实现了UserDao接口的目标对象的方法并且该接口不在com.zejian.dao包及其子包下
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao) !within(com.zejian.dao..*)")
private void myPointcut6(){}
//匹配了任意实现了UserDao接口的目标对象的方法并且该方法名称为addUser
@Pointcut("target(com.zejian.spring.springAop.dao.UserDao)&&execution(* com.zejian.spring.springAop.dao.UserDao.addUser(..))")
private void myPointcut7(){} 

五、通知传递参数

  • 在Spring AOP中,除了execution和bean指示符不能传递参数给通知方法,其他指示符都可以将匹配的方法相应参数或对象自动传递给通知方法。
  • 获取到匹配的方法参数后通过”argNames”属性指定参数名。如下,需要注意的是args(指示符)、argNames的参数名与before()方法中参数名 必须保持一致,即param。
  • 传递的参数可以简单类型或者对象,而且只有参数和目标方法也匹配时才会有值传递进来。
@Before(value="args(param)", argNames="param") //明确指定了    
public void before(int param) {    
    System.out.println("param:" + param);    
}  

当然也可以直接使用args指示符不带argNames声明参数,如下:

@Before("execution(public * com.zejian..*.addUser(..)) && args(userId,..)")  
public void before(int userId) {  
    //调用addUser的方法时如果与addUser的参数匹配则会传递进来会传递进来
    System.out.println("userId:" + userId);  
}  

args(userId,..)该表达式会保证只匹配那些至少接收一个参数而且传入的类型必须与userId一致的方法

参考:https://www.cnblogs.com/junzi2099/p/8274813.html

原文地址:https://www.cnblogs.com/wslook/p/9161930.html