Spring使用篇(七)—— 使用@AspectJ注解开发Spring AOP

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_36378917/article/details/87871703

1、开发环境搭建

  在Spring_Demo项目中创建名为“AspectJ”的module模块,并在lib文件夹中引入如下jar包,具体如下:
在这里插入图片描述
  然后在com.ccff.spring.aspectj.pojo包下创建名为“Role”的POJO类用于演示,具体代码如下所示:

package com.ccff.spring.aspectj.pojo;

public class Role {
    private Long roleId;
    private String roleName;
    private String roleNote;

    public Role() {
    }

    public Role(Long roleId, String roleName, String roleNote) {
        this.roleId = roleId;
        this.roleName = roleName;
        this.roleNote = roleNote;
    }

    public Long getRoleId() {
        return roleId;
    }

    public void setRoleId(Long roleId) {
        this.roleId = roleId;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getRoleNote() {
        return roleNote;
    }

    public void setRoleNote(String roleNote) {
        this.roleNote = roleNote;
    }

    @Override
    public String toString() {
        return "Role{" +
                "roleId=" + roleId +
                ", roleName='" + roleName + '\'' +
                ", roleNote='" + roleNote + '\'' +
                '}';
    }
}

2、选择连接点

  Spring是方法级别的AOP框架,而我们主要也是以某个类的某个方法作为连接点,用动态代理的理论来说,就是要拦截哪个方法织入对应AOP通知。为了更好的演示,首先在com.ccff.spring.aspectj.service包下创建名为“RoleService”的接口,具体代码如下:

package com.ccff.spring.aspectj.service;

import com.ccff.spring.aspectj.pojo.Role;

public interface RoleService {
    void printRole(Role role);
}

  这个接口十分简单,接下来为该接口提供一个名为“RoleServiceImpl”的实现类,具体代码如下:

package com.ccff.spring.aspectj.service;

import com.ccff.spring.aspectj.pojo.Role;
import org.springframework.stereotype.Component;

@Component
public class RoleServiceImpl implements RoleService {
    @Override
    public void printRole(Role role) {
        System.out.println(role.toString());
    }
}

  这个类没什么特别的,只是这个时候如果把printRole方法作为AOP的连接点,那么用动态地阿里的语言就是要为类RoleServiceImpl生成代理对象,然后拦截printRole方法,于是可以产生各种AOP通知方法。

3、创建切面

  选择好了连接点就可以创建切面了,对于动态代理的概念而言,它就如同一个拦截器在Spring中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面了。在com.ccff.spring.aspectj.aspect包下创建名为“RoleAspect”的切面,并使用注解@Aspect注解这个类,具体代码如下:

package com.ccff.spring.aspectj.aspect;

import org.aspectj.lang.annotation.*;

@Aspect
public class RoleAspect {
    @Before("execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..))")
    public void before(){
        System.out.println("Before方法执行......");
    }

    @After("execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..))")
    public void after(){
        System.out.println("After方法执行......");
    }

    @AfterReturning("execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..))")
    public void afterReturning(){
        System.out.println("AfterReturning方法执行......");
    }

    @AfterThrowing("execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..))")
    public void afterThrowing(){
        System.out.println("AfterThrowing方法执行......");
    }
}

  上面的代码中并没有环绕通知,各个注解详细信息如下表所示:

注解 通知 备注
@Before 在被代理对象的方法前调用 前置通知
@Around 在被代理对象的方法封装起来,并用环绕通知取代它 环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法,后续会讨论
@After 在被代理对象的方法后调用 后置通知
@AfterReturning 在被代理对象的方法正常返回后调用 返回通知,要求被代理对象的方法执行过程中没有发生异常
@AfterThrowing 在被代理对象的方法抛出异常后调用 异常通知,要求被代理对象的方法执行过程中发生异常

4、定义切点

  上一小节讨论了切面的组成,但是并没有详细讨论Spring是如何判断是否需要拦截方法的,毕竟并不是所有的方法都需要使用AOP编程,这就是一个确定连接点的问题。在切面中的注解定义了execution的正则表达式,Spring是通过这个正则表达式判断是否需要拦截你的方法的,这个表达式是:

execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..))

  依次对这个表达式做出分析:

  execution: 代表执行方法的时候会触发。

  *: 代表任意返回类型的方法。

  com.ccff.spring.aspectj.service.RoleServiceImpl: 代表类的全限定名。

  printRole: 被拦截的方法名称。

  (…): 任意的参数。

  显然通过上面的描述,全限定名为com.ccff.spring.aspectj.service.RoleServiceImpl的类的printRole方法被拦截了,这样它就按照AOP通知的规则把方法织入流程中。只是上述的表达式还有些简单,我们需要进一步讨论它们,它可以配置如下内容如下表所示:

AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的方法
@args() 限制连接点匹配参数为指定注解标注的执行方法
execution 用于匹配连接点的执行方法,这是最常用的匹配,可以通过类似上面的正则表达式匹配
this() 限制连接点匹配AOP代理的Bean,引用为指定类型的类
target 限制连接点匹配被代理对象为指定的类型
@target() 限制连接点匹配特定的执行对象,这些对象要符合指定的注解类型
within() 限制连接点匹配指定的包
@within() 限制连接点匹配指定的类型
@annotation 限定匹配带有指定注解的连接点

  注意,Spring只能支持上表列出的AspectJ指示器。如果使用了非表格中所列举的指示器,那么它将会抛出IllegalArgumentException异常。

  此外,Spring还根据自己的需求扩展了一个Bean()的指示器,使得我们可以根据bean id或者名称去定义对应的Bean,但是这里不再讲解。

  比如下面的例子,我们只需要对com.ccff.spring.aspectj包及其下面的包的类进行匹配。因此要修改前置通知,这样指示器就可以编写成如下所示:

@Before("execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..)) && within(com.ccff.spring.aspectj.*)")
public void before(){
    System.out.println("Before方法执行......");
}

  这里使用了within去限定了execution定义的正则表达式下的包的匹配,从而达到了限制效果,这样Spring只会拿到com.ccff.spring.aspectj.service包下面的类的printRole方法作为连接点了。&&代表并且的含义,如果使用XML方式引入,&在XML中具有特殊含义,因此可以用and代替它。运算符||可以用or代替,非运算符!可以使用not代替。

  上面的代码中的正则表达式需要重复书写多次,比较麻烦,其实只要引入另一个注解@Pointcut定义一个切点就可以避免这个麻烦,将RoleAspect类修改为如下所示:

package com.ccff.spring.aspectj.aspect;

import org.aspectj.lang.annotation.*;

@Aspect
public class RoleAspect {
    @Pointcut("execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..))")
    public void print(){
        
    }
    
    @Before("print()")
    public void before(){
        System.out.println("Before方法执行......");
    }

    @After("print()")
    public void after(){
        System.out.println("After方法执行......");
    }

    @AfterReturning("print()")
    public void afterReturning(){
        System.out.println("AfterReturning方法执行......");
    }

    @AfterThrowing("print()")
    public void afterThrowing(){
        System.out.println("AfterThrowing方法执行......");
    }
}

5、测试AOP

  接下来通过编写测试代码来测试SOP的内容。首先要对Spring的Bean进行配置,此处采用注解Java配置,具体代码如下所示:

package com.ccff.spring.aspectj.config;

import com.ccff.spring.aspectj.aspect.RoleAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.ccff.spring.aspectj")
public class AOPConfig {

    @Bean
    public RoleAspect roleAspect(){
        return new RoleAspect();
    }

}

  这里的注解@EnableAspectJAutoProxy代表着启用AspectJ框架的自动代理,这个时候Spring才会生成动态代理对象,进而可以使用AOP,而getRoleAspect方法,则生成一个切面实例。

  采用Java配置后能使Spring产生动态代理对象,从而阻止切面,把各类通知织入到流程中,接下来在com.ccff.spring.aspectj.test包下创建名为“AOPTest”的测试类,具体代码如下:

package com.ccff.spring.aspectj.test;

import com.ccff.spring.aspectj.config.AOPConfig;
import com.ccff.spring.aspectj.pojo.Role;
import com.ccff.spring.aspectj.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AOPTest {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class);
        RoleService roleService = (RoleService) context.getBean("roleService");
        Role role = new Role();
        role.setRoleId(1L);
        role.setRoleName("role-name-1");
        role.setRoleNote("role-note-1");
        roleService.printRole(role);
        System.out.println("===============测试afterThrowing方法========================");
        role = null;
        roleService.printRole(role);
    }
}

  运行上面的测试类,查看输出到控制台的日志信息如下,显然可以看到切面的通知已经通过AOP织入约定的流程当中了,这时可以使用AOP来处理一些需要切面的场景了。
在这里插入图片描述

6、环绕通知

  环绕通知是Spring AOP中最强大的通知,它可以同时实现前置通知和后置通知。它保留了调度被代理对象原有方法的功能,所以它既强大,又灵活。但是由于强大,它的可控性不那么强,如果不需要大量改变业务逻辑,一般而言并不需要使用它。

  修改com.ccff.spring.aspectj.aspect包下的切面类RoleAspect,为其添加环绕通知,具体代码如下:

package com.ccff.spring.aspectj.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class RoleAspect {
    @Pointcut("execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..))")
    public void print(){

    }

    @Before("print()")
    public void before(){
        System.out.println("Before方法执行......");
    }

    @After("print()")
    public void after(){
        System.out.println("After方法执行......");
    }

    @AfterReturning("print()")
    public void afterReturning(){
        System.out.println("AfterReturning方法执行......");
    }

    @AfterThrowing("print()")
    public void afterThrowing(){
        System.out.println("AfterThrowing方法执行......");
    }

    @Around("print()")
    public void around(ProceedingJoinPoint proceedingJoinPoint){
        System.out.println("Around方法执行前......");
        try {
            proceedingJoinPoint.proceed();
        }catch (Throwable throwable){
            throwable.printStackTrace();
        }
        System.out.println("Around方法执行后......");
    }
}

  通过上面的代码里面的注解@Around加入了切面的环绕通知,这个通知里面有一个ProceedingJoinPoint参数。这个参数是Spring提供的,使用它可以反射连接点方法。

  在加入环绕通知后,再次运行测试类,查看输出到控制台上的日志信息如下所示:
在这里插入图片描述
  从日志可以知道环绕通知使用proceedingJoinPoint.proceed();后会优先调度前置通知,然后反射切点方法,最后是后置通知和返回(或异常)通知。

7、织入

  织入是生成代理对象并将切面内容放入约定流程的过程。在之前的代码中,连接点所在的类都是拥有接口的类,而事实上即使没有接口,Spring也能提供AOP的功能,所以是否拥有接口不是使用Spring AOP的一个强制要求。

  如果使用JDK动态代理时,必须拥有接口,而使用CGLib则不需要,于是Spring就提供了一个规则:当类的实现存在接口的时候,Spring将提供JDK动态代理,从而织入各个通知;而当类不存在接口的时候没有办法使用JDK动态代理,Spring会采用CGLib来生成代理对象。

  动态代理对象是由Spring IoC容器根据描述生成的,一般不需要修改它,对于使用者而言,只要知道AOP术语中的约定便可以使用AOP了,只是在Spring中建议使用接口编程,这样的好处是使定义和实现相分离,有利于实现变化和替换,更为灵活一些。

8、给通知传递参数

  在Spring AOP各类通知中,除了环绕通知外,并没有讨论参数的传递,有时候我们还是希望能够传递参数的,为此,首先修改com.ccff.spring.aspectj.service包下的接口RoleService,具体代码如下所示:

package com.ccff.spring.aspectj.service;

import com.ccff.spring.aspectj.pojo.Role;

public interface RoleService {
    void printRole(Role role,int sort);
}

  通过上面的代码可以看出,在接口方法中添加了一个整型的排序参数,然后再修改该接口的实现类RoleServiceImpl,具体代码如下所示:

package com.ccff.spring.aspectj.service;

import com.ccff.spring.aspectj.pojo.Role;
import org.springframework.stereotype.Component;

@Component
public class RoleServiceImpl implements RoleService {
    @Override
    public void printRole(Role role,int sort) {
        System.out.println("排序参数sort的值为:"+sort+"——"+role.toString());
    }
}

  如想要将修改后的方法作为连接点,也就是使用切面拦截这个方法,因此修改com.ccff.spring.aspectj.aspect包下的RoleAspect切面类,以前置通知为例,具体代码如下所示:

package com.ccff.spring.aspectj.aspect;

import com.ccff.spring.aspectj.pojo.Role;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class RoleAspect {
    @Pointcut("execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..))")
    public void print(){

    }

    @Before("print()" + "&& args(role,sort)")
    public void before(Role role,int sort){
        System.out.println("Before方法执行:首先打印Role对象\n"+role.toString()+"\n然后打印整型排序数sort:"+sort+"\n");
    }

    @After("print()")
    public void after(){
        System.out.println("After方法执行......");
    }

    @AfterReturning("print()")
    public void afterReturning(){
        System.out.println("AfterReturning方法执行......");
    }

    @AfterThrowing("print()")
    public void afterThrowing(){
        System.out.println("AfterThrowing方法执行......");
    }

    @Around("print()")
    public void around(ProceedingJoinPoint proceedingJoinPoint){
        System.out.println("Around方法执行前......");
        try {
            System.out.println("Around方法执行......");
            proceedingJoinPoint.proceed();
        }catch (Throwable throwable){
            throwable.printStackTrace();
        }
        System.out.println("Around方法执行后......");
    }
}

  上面的代码中,在切点的定义中,加入了参数的定义,这样Spring就会解析这个正则表达式,然后将参数传递给方法了。

  最后修改com.ccff.spring.aspectj.test包下的测试类AOPTest,具体代码如下:

package com.ccff.spring.aspectj.test;

import com.ccff.spring.aspectj.check.RoleVerifier;
import com.ccff.spring.aspectj.config.AOPConfig;
import com.ccff.spring.aspectj.pojo.Role;
import com.ccff.spring.aspectj.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AOPTest {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class);
        RoleService roleService = (RoleService) context.getBean("roleService");
        Role role = new Role();
        role.setRoleId(1L);
        role.setRoleName("role-name-1");
        role.setRoleNote("role-note-1");
        roleService.printRole(role,1);
    }
}

  运行该测试方法,查看输出到控制台上的日志信息,可以看到在前置通知中可获得参数,具体如下所示:
在这里插入图片描述

9、引入

  Spring AOP只是通过动态代理技术,把各类通知织入到它所约定的流程当中,而事实上,有时候我们希望在目标类中引入其他类的方法来得到更好的实现,这时就可以引入其他的方法了。

  比如对于printRole方法,如果要求当前角色为空时不再打印,那么要引入一个新的检测器对其进行检测。在com.ccff.spring.aspectj.check包下创建名为“RoleVerifier”的接口,具体代码如下:

package com.ccff.spring.aspectj.check;

import com.ccff.spring.aspectj.pojo.Role;

public interface RoleVerifier {
    boolean verify(Role role);
}

  然后在该包下创建名为“RoleVerifierImpl”的实现类,实现RoleVerifier接口,具体代码如下所示:

package com.ccff.spring.aspectj.check;

import com.ccff.spring.aspectj.pojo.Role;

public class RoleVerifierImpl implements RoleVerifier {
    @Override
    public boolean verify(Role role) {
        return role != null;
    }
}

  接着改写com.ccff.spring.aspectj.aspect.RoleAspect包下的切面类RoleAspect,为其增加一个新的属性,具体代码如下所示:

package com.ccff.spring.aspectj.aspect;

import com.ccff.spring.aspectj.check.RoleVerifier;
import com.ccff.spring.aspectj.check.RoleVerifierImpl;
import com.ccff.spring.aspectj.pojo.Role;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class RoleAspect {
    @DeclareParents(value = "com.ccff.spring.aspectj.service.RoleServiceImpl+",defaultImpl = RoleVerifierImpl.class)
    public RoleVerifier roleVerifier;

    @Pointcut("execution(* com.ccff.spring.aspectj.service.RoleServiceImpl.printRole(..))")
    public void print(){

    }

    @Before("print()" + "&& args(role,sort)")
    public void before(Role role,int sort){
        System.out.println("Before方法执行:首先打印Role对象\n"+role.toString()+"\n然后打印整型排序数sort:"+sort+"\n");
    }

    @After("print()")
    public void after(){
        System.out.println("After方法执行......");
    }

    @AfterReturning("print()")
    public void afterReturning(){
        System.out.println("AfterReturning方法执行......");
    }

    @AfterThrowing("print()")
    public void afterThrowing(){
        System.out.println("AfterThrowing方法执行......");
    }

    @Around("print()")
    public void around(ProceedingJoinPoint proceedingJoinPoint){
        System.out.println("Around方法执行前......");
        try {
            System.out.println("Around方法执行......");
            proceedingJoinPoint.proceed();
        }catch (Throwable throwable){
            throwable.printStackTrace();
        }
        System.out.println("Around方法执行后......");
    }
}

  上面代码中注解@DeclareParents的配置如下:

  value = “com.ccff.spring.aspectj.service.RoleServiceImpl+”:表示对RoleServiceImpl类进行增强,也就是在RoleServiceImpl类中引入一个新的接口。

  defaultImpl:代表其默认的实现类,这里就是RoleVerifierImpl。

  最后修改com.ccff.spring.aspectj.test包下的测试类AOPTest,在打印角色方法之前先检测角色是否为空,具体代码如下所示:

package com.ccff.spring.aspectj.test;

import com.ccff.spring.aspectj.check.RoleVerifier;
import com.ccff.spring.aspectj.config.AOPConfig;
import com.ccff.spring.aspectj.pojo.Role;
import com.ccff.spring.aspectj.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AOPTest {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class);
        RoleService roleService = (RoleService) context.getBean("roleService");
        Role role = new Role();
        role.setRoleId(1L);
        role.setRoleName("role-name-1");
        role.setRoleNote("role-note-1");
        RoleVerifier roleVerifier = (RoleVerifier) roleService;
        if (roleVerifier.verify(role))
            roleService.printRole(role,1);
        else
            System.out.println("Role对象为空,不予打印");
        System.out.println("===============测试role为空========================");
        role = null;
        if (roleVerifier.verify(role))
            roleService.printRole(role,1);
        else
            System.out.println("Role对象为空,不予打印");
    }
}

  运行该测试方法,查看输出到控制台上的日志信息,具体如下所示:
在这里插入图片描述
  接下来简单分析一下它的原理,我们知道Spring AOP依赖于动态代理来实现,生成动态代理对象时通过类似于下面这行代码来实现的:

//生成代理对象,并绑定代理方法
return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterface(),this);

  obj.getClass().getInterface()意味着代理对象挂在多个接口之下,换句话说,只要Spring AOP让代理对象挂到RoleService和RoleVerifier两个接口之下,那么就可以把对应的Bean通过强制转换,让其在RoleService和RoleVerifier之间相互转换了。

  我们可以通过打断点调试程序进行验证,例如在如下代码中打入断点:
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_36378917/article/details/87871703