【JavaEE】面向切面编程

面向切面编程

一切从Spring AOP的底层技术——动态代理开始

一个简单的约定游戏

约定规则

首先提供一个Interceptor接口,定义如下:

public interface Interceptor {

	public void before(Object obj);

	public void after(Object obj);

	public void afterReturning(Object obj);

	public void afterThrowing(Object obj);
}

这是一个拦截接口,可以对它创建实现类。代码如下:

public class ProxyBeanFactory {
   public static <T> T getBean(T  obj, Interceptor interceptor) {
        return (T) ProxyBeanUtil.getBean(obj, interceptor);
    }
}

具体类ProxyBeanUtil的getBean方法的逻辑不需要去理会,当使用了这个方法后,存在如下约定,即当一个对象通过ProxyBeanFactory的getBean方法定义后,拥有这样的约定:

  • Bean必须是一个实现了某一个接口的对象
  • 最先会执行拦截器的before方法
  • 其次执行Bean的方法(通过反射的形式)
  • 执行Bean方法时,无论是否产生异常,都会执行after方法
  • 执行Bean方法时,如果不产生异常,则执行afterRunning方法;如果产生异常,则执行afterThrowing方法。

自己的代码

比如打印一个角色信息,由于约定服务对象必须实现接口,于是自定义一个RoleService接口,代码如下:

import com.ssm.chapter11.game.pojo.Role;

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

然后编写它的实现类,代码如下:

import com.ssm.chapter11.game.pojo.Role;
import com.ssm.chapter11.game.service.RoleService;

public class RoleServiceImpl implements RoleService {
   @Override
   public void printRole(Role role) {
      System.out.println(
            "{id =" + role.getId() + ", roleName=" + role.getRoleName() + ", note=" + role.getNote() + "}");
   }
}

这里还欠缺一个拦截器,代码如下:

import com.ssm.chapter11.game.Interceptor;

public class RoleInterceptor implements Interceptor {
   @Override
    public void before(Object obj) {
        System.out.println(
           "准备打印角色信息");
    }

    @Override
    public void after(Object obj) {
        System.out.println(
           "已经完成角色信息的打印处理");
    }

    @Override
    public void afterReturning(Object obj) {
         System.out.println(
             "刚刚完成打印功能,一切正常。");
    }

    @Override
    public void afterThrowing(Object obj) {
        System.out.println(
            "打印功能执行异常了,查看一下角色对象为空了吗?");
    }
}

测试代码如下:

import com.ssm.chapter11.game.interceptor.RoleInterceptor;
import com.ssm.chapter11.game.pojo.Role;
import com.ssm.chapter11.game.service.RoleService;
import com.ssm.chapter11.game.service.impl.RoleServiceImpl;

public class GameMain {
   public static void main(String[] args) {
      RoleService roleService = new RoleServiceImpl();
      Interceptor interceptor = new RoleInterceptor();
      RoleService proxy = ProxyBeanFactory.getBean(roleService, interceptor);
      Role role = new Role(1L, "role_name_1", "role_note_1");
      proxy.printRole(role);
      System.out.println("##############测试afterthrowing方法###############");
      role = null;
      proxy.printRole(role);
   }
}

使用动态代理实现流程

通过JDK动态代理实现上述流程,代码如下:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
class ProxyBeanUtil implements InvocationHandler {
    //被代理对象
    private Object obj;
    //拦截器
    private Interceptor interceptor = null;

    /**
     * 获取动态代理对象.
     * @param obj 被代理对象
     * @param interceptor 拦截器
     * @param aroundFlag 是否启用around方法
     * @return  动态代理对象
     */
    public static Object getBean(Object obj, Interceptor interceptor) {
        //使用当前类,作为代理方法,此时被代理对象执行方法的时候,会进入当前类的invoke方法里
        ProxyBeanUtil _this = new ProxyBeanUtil();
        //保存被代理对象
        _this.obj = obj;
        //保存拦截器
        _this.interceptor = interceptor;
        //生成代理对象,并绑定代理方法
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                obj.getClass().getInterfaces(), _this);
    }

    /**
     *  代理方法.
     * @param proxy 代理对象
     * @param method 当前调度方法
     * @param args 参数
     * @return 方法返回
     * @throws Throwable 异常 
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object retObj = null;
        //是否产生异常
        boolean exceptionFlag = false;
        //before方法
        interceptor.before(obj);
        try {
                //反射原有方法
                retObj = method.invoke(obj, args);
        } catch (Exception ex) {
           exceptionFlag = true;
        } finally {
            //after方法
            interceptor.after(obj);
        }
        if (exceptionFlag) {
           //afterThrowing方法
            interceptor.afterThrowing(obj);
        } else {
           //afterReturning方法
            interceptor.afterReturning(obj);
        }
        return retObj;
    }

}

首先, 通过getBean方法保存了被代理对象、拦截器(interceptor)和参数(args),为之后的调用奠定了基础。然后,生成了JDK动态代理对象(proxy),同时绑定了ProxyBeanUtil返回的对象作为其代理类,这样当代理对象调用方法的时候,就会进入到ProxyBeanUtil的invoke方法中,于是焦点又到了invoke方法上。
在invoke方法中,将拦截器的方法实现了一遍,其中设置了异常标志(exceptionFlag),通过这个标志就能判断反射原有对象方法的时候是否发生了异常。

Spring AOP的基本概念

AOP的概念和使用原因

AOP编程有者重要的意义,首先它可以拦截一些方法,然后把各个对象组织成一个整体。
首先了解正常执行SQL的逻辑步骤,一个正常的SQL是:

扫描二维码关注公众号,回复: 10748724 查看本文章
  • 打开通过数据库连接池获得数据库连接资源,并做一定的设置工作
  • 执行对应的SQL语句,对数据进行操作
  • 如果SQL执行过程中发生异常,回滚事务
  • 如果SQL执行过程中没有发生异常,最后提交事务
  • 到最后的阶段,需要关闭一些连接资源。
    如果通过AOP框架实现,则过程如下:
  • 当方法标注为@Transactional时,则方法启用数据库事务功能
  • 在默认的情况下(注意是默认的情况下,可以通过配置改变),如果原有方法出现异常,则回滚事务;如果没有发生异常,那么就提交事务,这样整个事务管理AOP就完成了整个流程,无须开发者编写任何代码去实现
  • 最后关闭数据库资源。
    这是使用最广的执行流程,符合约定优于配置的开发原则。在大部分的情况下,只需要使用默认的约定即可,或者进行一些特定的配置,来完成所需要的功能,这样对于开发者而言就更为关注业务开发,而不是资源控制、事务异常处理,这些AOP框架都可以完成。

AOP是通过动态代理模式,带来管控各个对象操作的切面环境,管理包括日志、数据库事务等操作,可以在反射原有对象方法之前正常返回、异常返回事后插入自己的逻辑代码,有时候甚至取代原始方法。在一些常用的流程中,比如数据库事务,AOP会提供默认的实现逻辑,也会提供一些简单的配置,程序员就能比较方便地修改默认的实现,达到符合真实应用的效果,这样就可以大大降低开发的工作量,提高代码的可读性和可维护性,将开发集中在业务逻辑上。

面向切面编程的术语

  1. 切面(Aspect)
    切面就是在一个怎么样的环境中工作,它可以定义后面需要介绍的各类通知、切点和引入等内容,然后Spring AOP会将其定义的内容织入到约定的流程中,在动态代理中可以把它理解成一个拦截器。
  2. 通知(Advice)
    通知是切面开启后,切面的方法。它根据在代理对象真实方法调用前、后的顺序和逻辑区分,它和约定游戏的例子里的拦截器的方法十分接近。
  • 前置通知(before):在动态代理反射原有对象方法或者执行环绕通知前执行的通知功能。
  • 后置通知(after):在动态代理反射原有对象方法或者执行环绕通知后执行的通知功能。无论是否抛出异常,它都会被执行。
  • 返回通知(afterReturning):在动态代理反射原有对象方法或者执行环绕通知后正常返回(无异常)执行的通知功能。
  • 异常通知(afterThrowing):在动态代理反射原有对象方法或者执行环绕通知产生异常后执行的通知功能。
  • 环绕通知(around):在动态代理中,它可以取代当前被拦截对象的方法,提供回调原有被拦截对象的方法。
  1. 引入(Introduction)
    引入允许在现有的类里添加自定义的类和方法。
  2. 切点(Pointcut)
    这是一个告诉Spring AOP在什么时候启动拦截并织入对应的流程中,因为并不是所有的开发都需要启动AOP的,它往往通过正则表达式进行限定。
  3. 连接点(join point)
    连接点对应的是具体需要拦截的东西。
  4. 织入(Weaving)
    织入是一个生成代理对象并将切面内容放入到流程中的过程,实际的代理可以分为静态代理和动态代理。静态代理是在编译class文件时生成的代码逻辑,但是在Spring中并不使用这样的方法。一种是通过ClassLoader也就是在类加载的时候生成的代码逻辑,但是它在应用程序代码运行前就生成对应的逻辑。还有一种是运行期,动态生成代码的方式,这是Spring AOP所采用的方式,Spring是以JDK和CGLIB动态代理来生成代理对象的。

Spring对AOP的支持

AOP并不是Spring框架特有的,Spring只是支持AOP编程的框架之一。Spring AOP是一种基于方法拦截的AOP,换句话说Spring只能支持方法拦截的AOP。在Spring中有4种方式去实现AOP的拦截功能。

  • 使用ProxyFactoryBean和对应的接口实现AOP
  • 使用XML配置AOP
  • 使用@AspectJ注解驱动切面
  • 使用AspectJ注入切面。
    在Spring AOP的拦截方法中,真正常用的是用@AspectJ注解的方式实现的切面,有时候XML配置也有一定的辅助作用。

使用@AspectJ注解开发Spring AOP

选择连接点

Spring是方法级别的AOP框架,而我们主要也是以某个类的某个方法作为连接点,用动态代理的理论来说,就是要拦截哪个方法织入对应AOP通知。首先,创建一个接口:

import com.ssm.chapter11.game.pojo.Role;

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

}

接下里,提供一个实现类:

import org.springframework.stereotype.Component;

import com.ssm.chapter11.aop.service.RoleService;
import com.ssm.chapter11.game.pojo.Role;

@Component
public class RoleServiceImpl implements RoleService {
   
   @Override
   public void printRole(Role role) {
      System.out.println("{id: " + role.getId() + ", " 
           + "role_name : " + role.getRoleName() + ", "
           + "note : " + role.getNote() + "}");
   }
}

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

创建切面

选择好了连接点就可以创建切面了,对于动态代理的概念而言,它就如同一个拦截器,在Spring中只要使用@AspectJ注解一个类,那么Spring IoC容器就会认为这是一个切面了。代码如下:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.DeclareParents;
import org.aspectj.lang.annotation.Pointcut;

import com.ssm.chapter11.aop.verifier.RoleVerifier;
import com.ssm.chapter11.aop.verifier.impl.RoleVerifierImpl;
import com.ssm.chapter11.game.pojo.Role;

@Aspect
public class RoleAspect {
   @Before("execution(*
   com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void before() {
      System.out.println("before ....");
   }

   @After("execution(*
   com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void after() {
      System.out.println("after ....");
   }

   @AfterReturning("execution(*
   com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void afterReturning() {
      System.out.println("afterReturning ....");
   }

   @AfterThrowing("execution(*
   com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void afterThrowing() {
      System.out.println("afterThrowing ....");
   }
}

这段代码中的注解使用了对应的正则式,这些正则式是切点的问题,也就是要告诉Spring AOP,需要拦截什么对象的什么方法。

定义切点

Spring是通过这个正则表达式判断是否需要拦截你的方法,这个表达式是:

execution(*
com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))

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

  • execution:代表执行方法的时候会触发
  • *:代表任意返回类型的方法
  • com.ssm.chapter11.aop.service.impl.RoleServiceImpl:代表类的全限定名
  • printRole:被拦截方法名称
  • (…):任意的参数
AspectJ指示器 描述
arg() 限制连接点匹配参数为指定类型的方法
@args() 限制而连接点匹配参数为指定注解标注的执行方法
execution 用于匹配连接点的执行方法,这是最常用的匹配,可以通过类似上面的正则式进行匹配
this() 限制连接点匹配AOP代理的Bean,引用为指定类型的类
target 限制连接点匹配被代理对象为指定的类型
@target() 限制连接点匹配特定的执行对象,这些对象要符合指定的注解类型
within() 限制连接点匹配指定的包
@within() 限制连接点匹配指定的类型
@annotation 限定匹配带有指定注解的连接点

注意,Spring只能支持上表列出的AspectJ的指示器,如果使用了非表格中所列举的指示器,那么它将会抛出IllegalArgumentException异常。
此外,Spring还根据自己的需求扩展了一个Bean()的指示器,使得我们可以根据bean id或者名称去定义对应的Bean。
例如,只需要对com.ssm.chapter11.aop.impl包及其下面的包的类进行匹配,因此要修改前置通知,如下所示:

@Before("execution(* com.ssm.chapter11.*.*.*.*.printRole(..)) 
	&& within(com.ssm.chapter11.aop.service.impl.*)"
public void before() {
   System.out.println("before ....");
}

使用within去限定了execution定义的正则表达式下的包的匹配,从而达到了限制效果。&&表示并且的含义,如果使用XML方式引入,&在XML中具有特殊含义,因此可以用and代替他。运算符||可以用or代替,非运算符!可以用not代替。
正则表达式需要重复书写多次,比较麻烦,引入另一个注解@Pointcut定义一个切点就可以避免这个麻烦,代码如下:

import com.ssm.chapter11.aop.verifier.RoleVerifier;
import com.ssm.chapter11.aop.verifier.impl.RoleVerifierImpl;
import com.ssm.chapter11.game.pojo.Role;

@Aspect
public class RoleAspect {
   
   @Pointcut("execution(* com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void print() {
   }

   @Before("print()")
   // @Before("execution(*
   // com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void before() {
      System.out.println("before ....");
   }

   @After("print()")
   // @After("execution(*
   // com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void after() {
      System.out.println("after ....");
   }

   @AfterReturning("print()")
   // @AfterReturning("execution(*
   // com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void afterReturning() {
      System.out.println("afterReturning ....");
   }

   @AfterThrowing("print()")
   // @AfterThrowing("execution(*
   // com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void afterThrowing() {
      System.out.println("afterThrowing ....");
   }
}

测试AOP

首先对Spring的Bean进行配置,采用注解Java配置,代码如下:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import com.ssm.chapter11.aop.aspect.RoleAspect;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.ssm.chapter11.aop")
public class AopConfig {
   
   @Bean
    public RoleAspect getRoleAspect() {
        return new RoleAspect();
    }
}

其中的@EnableAspectJAutoProxy代表着启用AspectJ框架的自动代理,这个时候Spring才会生成动态代理对象,进而可以使用AOP,而getRoleAspect方法,则生成一个切面实例。
Spring还提供了XML的方式,这里就需要使用AOP的命名空间了,配置如下:

<?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-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd        
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
   <aop:aspectj-autoproxy />
   <bean id="roleAspect" class="com.ssm.chapter11.aop.aspect.RoleAspect" />
   <bean id="roleService" class="com.ssm.chapter11.aop.service.impl.RoleServiceImpl" />
</beans>

无论用XML还是用Java的配置,都能使Spring产生动态代理对象,从而组织切面,把各类通知织入到流程当中。测试主函数如下:

private static void testAnnotation() {
   ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
   RoleService roleService = ctx.getBean(RoleService.class);
   Role role = new Role();
   role.setId(1L);
   role.setRoleName("role_name_1");
   role.setNote("note_1");
   roleService.printRole(role);
   System.out.println("#################");
   // 测试异常通知
   role = null;
   roleService.printRole(role);
}

环绕通知

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

@Around("print()")
public void around(ProceedingJoinPoint jp) {
   System.out.println("around before ....");
   try {
      jp.proceed();
   } catch (Throwable e) {
      e.printStackTrace();
   }
   System.out.println("around after ....");
}

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

织入

织入是生成代理对象并将切面内容放入约定流程的过程。在上述代码中,连接点所在的类都是拥有接口的类,而事实上即使没有接口,Spring也能提供AOP的功能,所以是否拥有接口不是使用Spring AOP的一个强制要求。于是Spring提供了一个规则:当类的实现存在接口的时候,Spring将提供JDK动态代理,从而织入各个通知;而当类不存在接口的时候没有办法使用JDK动态代理,Spring会采用CGLIB来生成代理对象。
动态代理对象是由Spring IoC容器根据描述生成的,一般不需要修改它。

给通知传递参数

在Spring AOP各类通知中,除了环绕通知外,并没有讨论参数的传递,有时候还是希望能够传递参数的。示例代码如下,修改连接点为一个多参数的方法:

public void printRole(Role role, int sort) {
   System.out.println("{id: " + role.getId() + ", " 
        + "role_name : " + role.getRoleName() + ", "
        + "note : " + role.getNote() + "}");
   System.out.println(sort);
}

这里存在两个参数,一个是角色,一个是整型排序参数,那么要把这个方法作为连接点,也就是使用切面拦截这个方法,定义切点如下,以前置通知为例:

@Before("execution(* com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..)) " + "&& args(role, sort)")
public void before(Role role, int sort) {
   System.out.println("before ....");
}

引入

先定义一个RoleVerifier接口:

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

创建一个实现类RoleVerifierImpl:

public class RoleVerifierImpl implements RoleVerifier {
   @Override
   public boolean verify(Role role) {
      System.out.println("引入,检测一下角色是否为空");
      return role != null;
   }
}

在其RoleAspect类中加入一个新的属性,代码如下:

@DeclareParents(value= "com.ssm.chapter11.aop.service.impl.RoleServiceImpl+", defaultImpl=RoleVerifierImpl.class)
public RoleVerifier roleVerifier;

注解@DeclareParents的配置如下:

  • value=“com.ssm.chapter11.aop.service.impl.RoleServiceImpl+”:表示对RoleServiceImpl类进行增强,也就是在RoleServiceImpl中引入一个新的接口
  • defaultImpl:代表其默认的实现类,这里是RoleVerifierImpl。
    然后对这个方法进行测试,代码如下:
ApplicationContext ctx = new AnnotationConfigApplicationContext (AopConfig.class);
RoleService roleService = ctx.getBean(RoleService.class);
RoleVerifier roleVerifier = (RoleVerifier) roleService;
Role role = new Role();
role.setId(1L);
role.setRoleName("role_name_1");
role.setNote("note_1");
if (roleVerifier.verify(role)) {
    roleService.printRole(role);
}

使用强制转换之后就可以把roleService转化为RoleVerifier接口对象,然后就可以使用verify方法了。而RoleVerifier调用的方法verify,显然它就是通过RoleVerifierImpl来实现的。
分析它的原理,我们知道Spring AOP依赖于动态代理来实现,生成动态代理对象是通过类似于下面这行代码来实现的:

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

obj.getClass().getInterfaces()意味着代理对象挂在多个接口之下,换句话说,只要Spring AOP让代理对象挂在RoleService和RoleVerifier两个接口下,那么就可以把对应的Bean通过强制转换,让其在RoleService和RoleVerifier之间互相转换了。
同样的如果RoleServiceImpl没有接口,那么它也会使用CGLIB动态代理,使用增强者(Enhancer)也会由一个interfaces的属性,允许代理对象挂到对应的多个接口下,于是也可以按照JDK动态代理那样使得对象可以在多个接口之间相互转换。

使用XML配置开发Spring AOP

使用XML方式开发AOP,其实它们的原理是相同的。这里需要在XML中引入AOP的命名空间,下表是XML配置AOP的元素:

AOP配置元素 用途 备注
aop:advisor 定义AOP的通知器 一种较老的方式,目前很少使用
aop:aspect 定义一个切面
aop:before 定义前置通知
aop:after 定义后置通知
aop:around 定义环绕方式
aop:after-returning 定义返回通知
aop:after-throwing 定义异常通知
aop:config 顶层的AOP配置元素 AOP的配置是以它为开始的
aop:declare-parents 给通知引入新的额外接口,增强功能
aop:pointcut 定义切点

先定义要拦截的类和方法,尽管Spring并不强迫定义接口使用AOP,但是建议使用接口,有利于实现和定义相分离,使得系统更为灵活。
首先定义一个新的接口,代码如下:

import com.ssm.chapter11.game.pojo.Role;

public interface RoleService {

   public void printRole(Role role);
}

然后给出实现类,代码如下:

import com.ssm.chapter11.game.pojo.Role;
import com.ssm.chapter11.xml.service.RoleService;

public class RoleServiceImpl implements RoleService {

   @Override
   public void printRole(Role role) {
      System.out.print("id = " + role.getId()+",");
      System.out.print("role_name = " + role.getRoleName()+",");
      System.out.println("note = " + role.getNote());
   }

}

通过AOP来增强它的功能,为此需要一个切面类,代码如下:

import org.aspectj.lang.ProceedingJoinPoint;

public class XmlAspect {

   public void before() {
      System.out.println("before ......");
   }
   
   public void after() {
      System.out.println("after ......");
   }
   
   public void afterThrowing() {
      System.out.println("after-throwing ......");
   }
   
   public void afterReturning() {
      System.out.println("after-returning ......");
   }
}

同样的也没有任何的注解,这就意味着需要我们使用XML去向Spring IoC容器描述它们。

前置通知、后置通知、返回通知和异常通知

配置代码如下:

<?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-4.0.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

   <bean id="xmlAspect" class="com.ssm.chapter11.xml.aspect.XmlAspect" />
   <bean id="roleService" class="com.ssm.chapter11.xml.service.impl.RoleServiceImpl" />

   <aop:config>     
   <!--引用xmlAspect作为切面-->
      <aop:aspect ref="xmlAspect">
         <aop:before method="before"
            pointcut="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
         <aop:after method="after"
            pointcut="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
         <aop:after-throwing method="afterThrowing"
            pointcut="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
         <aop:after-returning method="afterReturning"
            pointcut="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
         </aop:aspect>
   </aop:config>
 </beans>

这里首先通过引入的XML定义了AOP的命名空间,然后定义了一个roleService类和切面xmlAspect类,最后通过aop:config\取定义AOP的内容信息。
和使用注解一样,也可以通过定义切点,然后引用到别的通知上。代码如下:

<aop:config>
   <aop:aspect ref="xmlAspect">
       <!-- 自定义切点 -->
      <aop:pointcut id="printRole"
         expression="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
      <!-- 定义通知 -->
      <aop:before method="before" pointcut-ref="printRole" />
      <aop:after method="after" pointcut-ref="printRole" />
      <aop:after-throwing method="afterThrowing"
         pointcut-ref="printRole" />
      <aop:after-returning method="afterReturning"
         pointcut-ref="printRole" />
      <aop:around method="around" pointcut-ref="printRole" />
     </aop:aspect>
</aop:config>

通过这段代码可以定义切点并进行引入,这样就可以避免多次书写同一正则式的麻烦。

环绕通知

和其他通知一样,环绕通知也可以织入到约定的流程当中,代码如下:

public void around(ProceedingJoinPoint jp){
	System.out.println("around before......");
	try{
		jp.proceed();
	}catch (Throwable e){
		new RuntimeException("回调原有流程,产生异常...");
	}
	System.out.println("around after......");
}

通过调度ProceedingJointPoint的proceed方法就能够调用原有的方法了。加入环绕通知的配置如下:

<aop:around method="around" pointcut-ref="printRole" />

测试代码如下:

public static void main(String []args) {
   ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg4.xml");
   RoleService roleService = ctx.getBean(RoleService.class);
   Role role = new Role();
   role.setId(1L);
   role.setRoleName("role_name_1");
   role.setNote("note_1");
   roleService.printRole(role);
}

这里读入了XML文件,然后通过容器获取了Bean,创建了角色了,然后打印角色,就能得到日志。显然所有的通知都被织入了AOP所约定了流程,但是请注意,环绕通知的before是在前置通知之后打印出来的。

给通知传递参数

通过XML的配置,也可以引入参数到通知当中。首先,改写代码如下:

public void before(Role role){
	System.out.println("role_id="+role.getId()+"before ......")}

此时带上了参数role,修改前置通知的配置代码如下:

<aop:before method="before" pointcut="execution(*
com.ssh.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))  and
args(role)" />

引入

无论是使用JDK动态代理,还是使用CGLIB动态代理都可以将代理对象下挂到多个接口之下,这样就能够引入新的方法了。
在代码中加入一个新的属性RoleVerifier类对象:

public RoleVerifier roleVerifier = null;

此时可以使用XML配置它,配置的内容和注解引入的方法相当,它是使用<aop:declare-parents>去引入的,代码如下:

<aop:declare-parents
   types-matching="com.ssm.chapter11.xml.service.impl.RoleServiceImpl+"
   implement-interface="com.ssm.chapter11.aop.verifier.RoleVerifier"
   default-impl="com.ssm.chapter11.aop.verifier.impl.RoleVerifierImpl" />

经典Spring AOP应用程序

先定义一个类来实现前置通知,它要求类实现MethodBeforeAdvice接口的before方法,代码如下:

import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;

public class ProxyFactoryBeanAspect implements MethodBeforeAdvice {

   @Override
   /***
    * 前置通知
    * @param method 被拦截方法(切点)
    * @param params 参数 数组[role]
    * @param roleService 被拦截对象
    */
   public void before(Method method, Object[] params, Object roleService) throws Throwable {
      System.out.println("前置通知!!");
   }
}

有了它还需要对Spring IoC容器描述对应的信息,这时候需要一个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:p="http://www.springframework.org/schema/p"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
   http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

   <bean id="proxyFactoryBeanAspect" class="com.ssm.chapter11.aspect.ProxyFactoryBeanAspect" />

   <!--设定代理类 -->
   <bean id="roleService" class="org.springframework.aop.framework.ProxyFactoryBean">
      <!--这里代理的是接口 -->
      <property name="proxyInterfaces">
         <value>com.ssm.chapter11.game.service.RoleService</value>
      </property>

      <!--是ProxyFactoryBean要代理的目标类 -->
      <property name="target">
         <bean class="com.ssm.chapter11.game.service.impl.RoleServiceImpl" />
      </property>

      <!--定义通知 -->
      <property name="interceptorNames">
         <list>
            <!-- 引入定义好的spring bean -->
            <value>proxyFactoryBeanAspect</value>
         </list>
      </property>
   </bean>
</beans>

测试代码如下:

public static void main(String[] args) {
   ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
   Role role = new Role();
   role.setId(1L);
   role.setRoleName("role_name");
   role.setNote("note");
   RoleService roleService = (RoleService) ctx.getBean("roleService");
   roleService.printRole(role);
}

这样Spring AOP就被用起来了,它虽然很经典,但是已经不是主流方式。

多个切面

Spring也能支持多个切面。当有多个切面时,在测试过程中发现它不会存在任何顺序,这些顺序代码会随机生成,但是有时候希望它按照指定的顺序运行。在此之前要先定义一个连接点,为此新建一个接口——MultiBean,它十分简单,代码如下:

public interface MultiBean {
   public void testMulti();
}

实现接口:

import org.springframework.stereotype.Component;

import com.ssm.chapter11.multi.bean.MultiBean;

@Component
public class MultiBeanImpl implements  MultiBean {

   @Override
   public void testMulti() {
      System.out.println("test multi aspects!!");
   }

}

定义好连接点,然后需要切面:Aspect1、Aspect2和Aspect3进行AOP编程,这3个切面的定义如下:

@Aspect
public class Aspect1 {
   @Pointcut("execution(* com.ssm.chapter11.multi.bean.impl.MultiBeanImpl.testMulti(..))")
   public void print() {
   }
   
   @Before("print()")
   public void before() {
      System.out.println("before 1 ......");
   }
   
   @After("print()")
   public void after() {
      System.out.println("after 1 ......");
   }
   
   @AfterThrowing("print()")
   public void afterThrowing() {
      System.out.println("afterThrowing 1 ......");
   }
   
   @AfterReturning("print()") 
   public void afterReturning() {
      System.out.println("afterReturning 1 ......");
   }
}
@Aspect
public class Aspect2 {
   
   @Pointcut("execution(* com.ssm.chapter11.multi.bean.impl.MultiBeanImpl.testMulti(..))")
   public void print() {
   }
   
   @Before("print()")
   public void before() {
      System.out.println("before 2 ......");
   }
   
   @After("print()")
   public void after() {
      System.out.println("after 2 ......");
   }
   
   @AfterThrowing("print()")
   public void afterThrowing() {
      System.out.println("afterThrowing 2 ......");
   }
   
   @AfterReturning("print()") 
   public void afterReturning() {
      System.out.println("afterReturning 2 ......");
   }
}
@Aspect
public class Aspect3 {

   @Pointcut("execution(* com.ssm.chapter11.multi.bean.impl.MultiBeanImpl.testMulti(..))")
   public void print() {
   }

   @Before("print()")
   public void before() {
      System.out.println("before 3 ......");
   }

   @After("print()")
   public void after() {
      System.out.println("after 3 ......");
   }

   @AfterThrowing("print()")
   public void afterThrowing() {
      System.out.println("afterThrowing 3 ......");
   }

   @AfterReturning("print()")
   public void afterReturning() {
      System.out.println("afterReturning 3 ......");
   }
}

Java环境配置代码如下:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.ssm.chapter11.multi")
public class MultiConfig {

   @Bean
    public Aspect1 getAspect1() {
        return new Aspect1();
    }
   
   @Bean
    public Aspect2 getAspect2() {
        return new Aspect2();
    }  
   @Bean
    public Aspect3 getAspect3() {
        return new Aspect3();
    }
}

通过AnnotationConfigApplicationContext加载配置文件,显然多个切面是无序的。如何让它有序执行,在Spring中有多种方法,如果使用注解的切面,那么可以给切面加入注解@Ordered。例如:

@Aspect
@Order(1)
public class Aspect1 {
......

以此类推。

在Spring AOP的实现方式是动态代理,换句话说,Spring底层也是通过责任链模式来处理多个切面,事实上,还有其他的方法,比如也可以让切面实现Ordered(org.springframework.core.Ordered)接口,它定义了一个getOrder方法,如果需要取代Aspect1中的@Order(1)的功能,那么代码修改为:

/******* imports *******/
@Aspect
public class Aspect1 implements Ordered{
	@Override
	public int getOrder(){
		return 1;
	}
}
......

显然,没有使用@Order注解方便,另外也可以在XML文件中配置,示例如下:

<aop:aspect ref="aspect1" order="1">
......
</aop:aspect>
发布了279 篇原创文章 · 获赞 169 · 访问量 32万+

猜你喜欢

转载自blog.csdn.net/ARPOSPF/article/details/104910423