Spring02-面向切面编程(AOP)

面向切面编程[AOP]

代码冗余与装饰器模式

代码冗余现象

我们的Service层实现类中的每个方法都要加上事务控制,这样使得每个方法的前后都要加上重复的事务控制的代码,如下:

@Override
public void saveAccount(Account account) {
	try {
		TransactionManager.beginTransaction();
		accountDao.save(account);		// 唯一的一行业务代码
		TransactionManager.commit();
	} catch (Exception e) {
		TransactionManager.rollback();
		e.printStackTrace();
	}finally {
		TransactionManager.release();
	}
}

@Override
public void updateAccount(Account account) {
	try {
		TransactionManager.beginTransaction();
		accountDao.update(account);		// 唯一的一行业务代码
		TransactionManager.commit();
	} catch (Exception e) {
		TransactionManager.rollback();
		e.printStackTrace();
	}finally {
		TransactionManager.release();
	}
}

@Override
public void deleteAccount(Integer accountId) {
	try {
		TransactionManager.beginTransaction();
		accountDao.delete(accountId);	// 唯一的一行业务代码
		TransactionManager.commit();
	} catch (Exception e) {
		TransactionManager.rollback();
		e.printStackTrace();
	}finally {
		TransactionManager.release();
	}
}

我们发现出现了两个问题:

  1. 业务层方法变得臃肿了,里面充斥着很多重复代码.
  2. 业务层方法和事务控制方法耦合了. 若提交,回滚,释放资源中任何一个方法名变更,都需要修改业务层的代码.

因此我们引入了装饰模式解决代码冗余和耦合现象.

解决代码冗余的思路: 装饰模式和动态代理

动态代理的写法

常用的动态代理分为两种

  1. 基于接口的动态代理,使用JDK 官方的 Proxy 类,要求被代理者至少实现一个接口.

    • 基于接口的动态代理:
      • 涉及的类:Proxy
      • 提供者:JDK官方
    • 如何创建代理对象:
      • 使用Proxy类中的newProxyInstance方法
    • 创建代理对象的要求:
      • 被代理类最少实现一个接口,如果没有则不能使用
    • newProxyInstance方法的参数:
      • ClassLoader:类加载器
        • 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
      • Class[]:字节码数组
        • 它是用于让代理对象和被代理对象有相同方法。固定写法。
      • InvocationHandler:用于提供增强的代码
        • 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
        • 此接口的实现类都是谁用谁写。
    接口名 新对象名 = (接口名)Proxy.newProxyInstance(
        被代理的对象.getClass().getClassLoader(),	// 被代理对象的类加载器,固定写法
        被代理的对象.getClass().getInterfaces(),	// 被代理对象实现的所有接口,固定写法
        new InvocationHandler() {	// 匿名内部类,通过拦截被代理对象的方法来增强被代理对象
            /* 被代理对象的任何方法执行时,都会被此方法拦截到
            	其参数如下:
                    proxy: 代理对象的引用,不一定每次都用得到
                    method: 被拦截到的方法对象
                    args: 被拦截到的方法对象的参数
            	返回值:
            		被增强后的返回值
    		*/
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              	//提供增强的代码
                Object returnValue = null;
                if("方法名".equals(method.getName())) {
                	// 增强方法的操作
                    rtValue = method.invoke(被代理的对象, args);
                    // 增强方法的操作
                    return rtValue;
                }          
            }
        });
    
  2. 基于子类的动态代理,使用第三方的 CGLib库,要求被代理类不能是final类.

    • 基于子类的动态代理:
      • 涉及的类:Enhancer
      • 提供者:第三方cglib库
    • 如何创建代理对象:
      • 使用Enhancer类中的create方法
    • 创建代理对象的要求:
      • 被代理类不能是final类
    • create方法的参数:
      • Class:字节码
        • 它是用于指定被代理对象的字节码。
      • Callback:用于提供增强的代码
        • 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
        • 此接口的实现类都是谁用谁写。
        • 我们一般写的都是该接口的子接口实现类:MethodInterceptor
    final Producer producer = new Producer(); // 这个代理对象没有实现接口
    Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
      /**
                 * 执行该代理对象的任何方法都会经过该方法
                 * @param proxy
                 * @param method
                 * @param args
                 *    以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
                 * @param methodProxy :当前执行方法的代理对象
                 * @return
                 * @throws Throwable
                 */
      @Override
      public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //提供增强的代码
        Object returnValue = null;
    
        //1.获取方法执行的参数
        Float money = (Float)args[0];
        //2.判断当前方法是不是销售
        if("saleProduct".equals(method.getName())) {
          returnValue = method.invoke(producer, money*0.8f);
        }
        return returnValue;
      }
    });
    cglibProducer.saleProduct(12000f);
    

使用动态代理解决代码冗余现象

我们使用动态代理对上述Service进行改造,创建BeanFactory类作为service层对象工厂,通过其getAccountService方法得到业务层对象.

// 用于创建Service的代理对象的工厂
public class BeanFactory {

	private IAccountService accountService;		// 被增强的service对象
	private TransactionManager txManager;		// 事务控制工具类

	// 成员变量的set方法,以便Spring容器注入
	public void setTxManager(TransactionManager txManager) {
		this.txManager = txManager;
	}
	public final void setAccountService(IAccountService accountService) {
		this.accountService = accountService;
	}

	// 获取增强后的Service对象
	public IAccountService getAccountService() {
		return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
			accountService.getClass().getInterfaces(),
			new InvocationHandler() {
				// 增强方法
				@Override
				public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
					Object rtValue = null;
					try {
						//1.开启事务
						txManager.beginTransaction();
						//2.执行操作
						rtValue = method.invoke(accountService, args);
						//3.提交事务
						txManager.commit();
						//4.返回结果
						return rtValue;
					} catch (Exception e) {
						//5.回滚操作
						txManager.rollback();
						throw new RuntimeException(e);
					} finally {
						//6.释放连接
						txManager.release();
					}
				}
			});
	}
}

bean.xml中,添加如下配置

<!--配置beanfactory-->
<bean id="beanFactory" class="cn.maoritian.factory.BeanFactory">
	<!-- 注入service -->
    <property name="accountService" ref="accountService"></property>
    <!-- 注入事务控制工具 -->
    <property name="txManager" ref="txManager"></property>
</bean>

这样,我们就可以通过Spring的IOC获取增强后的Service对象.

使用SpringAOP解决代码冗余

AOP相关术语

  • Joinpoint(连接点): 被拦截到的方法.

  • Pointcut(切入点): 我们对其进行增强的方法.

  • Advice(通知/增强): 对切入点进行的增强操作

    包括前置通知,后置通知,异常通知,最终通知,环绕通知

  • Weaving(织入): 是指把增强应用到目标对象来创建新的代理对象的过程.

  • Aspect(切面): 是切入点和通知的结合.

使用XML配置AOP

使用XML配置AOP的步骤

bean.xml中配置AOP要经过以下几步:

  1. bean.xml中引入约束并将通知类注入Spring容器中

    <?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">
    	
        <!--通知类-->
    	<bean id="logger" class="cn.maoritian.utils.Logger"></bean>
    </beans>
    
  2. 使用<aop:config>标签声明AOP配置,所有关于AOP配置的代码都写在<aop:config>标签内

    <aop:config>
    	<!-- AOP配置的代码都写在此处 -->
    </aop:config>
    
  3. 使用<aop:aspect>标签配置切面,其属性如下

    • id: 指定切入点表达式的id
    • ref: 引用通知类的id
    <aop:config>
    	<aop:aspect id="logAdvice" ref="logger">
        	<!--配置通知的类型要写在此处-->
        </aop:aspect>
    </aop:config>
    
  4. 使用<aop:pointcut>标签配置切入点表达式,指定对哪些方法进行增强,其属性如下

    • id: 指定切入点表达式的id
    • expression: 指定切入点表达式
    <aop:config>
      <aop:aspect id="logAdvice" ref="logger">
        <aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
      </aop:aspect>
    </aop:config>
    
  5. 使用<aop:xxx>标签配置对应类型的通知方法

    1. 其属性如下:

      1. method: 指定通知类中的增强方法名.
      2. ponitcut-ref: 指定切入点的表达式的id
      3. poinitcut: 指定切入点表达式

      其中pointcut-refpointref属性只能有其中一个

    2. 具体的通知类型:

      1. <aop:before>: 配置前置通知,指定的增强方法在切入点方法之前执行.
      2. <aop:after-returning>: 配置后置通知,指定的增强方法在切入点方法正常执行之后执行.
      3. <aop:after-throwing>: 配置异常通知,指定的增强方法在切入点方法产生异常后执行.
      4. <aop:after>: 配置最终通知,无论切入点方法执行时是否发生异常,指定的增强方法都会最后执行.
      5. <aop:around>: 配置环绕通知,可以在代码中手动控制增强代码的执行时机.
      <aop:config>
          <aop:aspect id="logAdvice" ref="logger">
              <!--指定切入点表达式-->
              <aop:pointcut expression="execution(* cn,maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
              <!--配置各种类型的通知-->
              <aop:before method="printLogBefore" pointcut-ref="pt1"></aop:before>
              <aop:after-returning method="printLogAfterReturning" pointcut-ref="pt1"></aop:after-returning>
              <aop:after-throwing method="printLogAfterThrowing" pointcut-ref="pt1"></aop:after-throwing>
              <aop:after method="printLogAfter" pointcut-ref="pt1"></aop:after>
      		<!--环绕通知一般单独使用-->       
              <!-- <aop:around method="printLogAround" pointcut-ref="pt1"></aop:around> -->
          </aop:aspect>
      </aop:config>
      

切入点表达式

  • 切入点表达式的写法: execution([修饰符] 返回值类型 包路径.类名.方法名(参数))

  • 切入点表达式的省略写法:

    • 全匹配方式:

      <aop:pointcut expression="execution(public void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 其中访问修饰符可以省略:

      <aop:pointcut expression="execution(void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 返回值可使用*,表示任意返回值:

      <aop:pointcut expression="execution(* cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 包路径可以使用*,表示任意包. 但是*.的个数要和包的层级数相匹配

      <aop:pointcut expression="execution(*  *.*.*.*.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 包路径可以使用*..,表示当前包,及其子包(因为本例子中将bean.xml放在根路径下,因此..可以匹配项目内所有包路径)

      <aop:pointcut expression="execution(*  *..AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
      
    • 类名可以使用*,表示任意类

      <aop:pointcut expression="execution(* *..*.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
      
    • 方法名可以使用*,表示任意方法

      <aop:pointcut expression="execution(* *..*.*(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
      
    • 参数列表可以使用*,表示参数可以是任意数据类型,但是必须存在参数

      <aop:pointcut expression="execution(* *..*.*(*))" id="pt1"></aop:pointcut>
      
    • 参数列表可以使用..表示有无参数均可,有参数可以是任意类型

      <aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
      
    • 全通配方式,可以匹配匹配任意方法

      <aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
      
  • 切入点表达式的一般写法

    一般我们都是对业务层所有实现类的所有方法进行增强,因此切入点表达式写法通常为

    <aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
    

环绕通知

  1. 前置通知,后置通知,异常通知,最终通知的执行顺序

    Spring是基于动态代理对方法进行增强的,前置通知,后置通知,异常通知,最终通知在增强方法中的执行时机如下:

    // 增强方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        Object rtValue = null;
        try {
            // 执行前置通知
            
            // 执行原方法
            rtValue = method.invoke(accountService, args); 
            
            // 执行后置通知
            return rtValue;
        } catch (Exception e) {
            // 执行异常通知
        } finally {
            // 执行最终通知
        }
    }
    
  2. 环绕通知允许我们更自由地控制增强代码执行的时机

    Spring框架为我们提供一个接口ProceedingJoinPoint,它的实例对象可以作为环绕通知方法的参数,通过参数控制被增强方法的执行时机.

    1. ProceedingJoinPoint对象的getArgs()方法返回被拦截的参数
    2. ProceedingJoinPoint对象的proceed()方法执行被拦截的方法
    // 环绕通知方法,返回Object类型
    public Object printLogAround(ProceedingJoinPoint pjp) {
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();        
            printLogBefore();			// 执行前置通知
        	rtValue = pjp.proceed(args);// 执行被拦截方法
            printLogAfterReturn();		// 执行后置通知
        }catch(Throwable e) {
            printLogAfterThrowing();	// 执行异常通知
        }finally {
            printLogAfter();			// 执行最终通知
        }
        return rtValue;
    }
    

使用注解配置AOP

半注解配置AOP

Spring注解配置AOP的步骤

半注解配置AOP,需要在bean,xml中加入下面语句开启对注解AOP的支持

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

Spring用于AOP的注解

用于声明切面的注解

@Aspect: 声明当前类为通知类,该类定义了一个切面.相当于xml配置中的<aop:aspect>标签

@Component("logger")
@Aspect
public class Logger {
    // ...
}
用于声明通知的注解
  • @Before: 声明该方法为前置通知.相当于xml配置中的<aop:before>标签
  • @AfterReturning: 声明该方法为后置通知.相当于xml配置中的<aop:after-returning>标签
  • @AfterThrowing: 声明该方法为异常通知.相当于xml配置中的<aop:after-throwing>标签
  • @After: 声明该方法为最终通知.相当于xml配置中的<aop:after>标签
  • @Around: 声明该方法为环绕通知.相当于xml配置中的<aop:around>标签

属性:

  • value: 用于指定切入点表达式切入点表达式的引用

    @Component("logger")
    @Aspect	//表示当前类是一个通知类
    public class Logger {
    
        // 配置前置通知
    	@Before("execution(* cn.maoritian.service.impl.*.*(..))")
        public void printLogBefore(){
            System.out.println("前置通知Logger类中的printLogBefore方法开始记录日志了。。。");
        }
    
        // 配置后置通知
        @AfterReturning("execution(* cn.maoritian.service.impl.*.*(..))")
        public void printLogAfterReturning(){
            System.out.println("后置通知Logger类中的printLogAfterReturning方法开始记录日志了。。。");
        }
        
        // 配置异常通知
    	@AfterThrowing("execution(* cn.maoritian.service.impl.*.*(..))")
        public void printLogAfterThrowing(){
            System.out.println("异常通知Logger类中的printLogAfterThrowing方法开始记录日志了。。。");
        }
    
        // 配置最终通知
        @After("execution(* cn.maoritian.service.impl.*.*(..))")
        public void printLogAfter(){
            System.out.println("最终通知Logger类中的printLogAfter方法开始记录日志了。。。");
        }
    
        // 配置环绕通知
        @Around("execution(* cn.maoritian.service.impl.*.*(..))")
        public Object aroundPringLog(ProceedingJoinPoint pjp){
            Object rtValue = null;
            try{
                Object[] args = pjp.getArgs();	
    			printLogBefore();				// 执行前置通知
                rtValue = pjp.proceed(args);	// 执行切入点方法
    			printLogAfterReturning();		// 执行后置通知
                return rtValue;
            }catch (Throwable t){
                printLogAfterThrowing();		// 执行异常通知
                throw new RuntimeException(t);
            }finally {
                printLogAfter();				// 执行最终通知
            }
        }
    }
    
用于指定切入点表达式的注解

@Pointcut: 指定切入点表达式,其属性如下:

  • value: 指定表达式的内容

@Pointcut注解没有id属性,通过调用被注解的方法获取切入点表达式.

@Component("logger")
@Aspect	//表示当前类是一个通知类
public class Logger {

    // 配置切入点表达式
    @Pointcut("execution(* cn.maoritian.service.impl.*.*(..))")
    private void pt1(){} 
    
    // 通过调用被注解的方法获取切入点表达式
	@Before("pt1()")
    public void printLogBefore(){
        System.out.println("前置通知Logger类中的printLogBefore方法开始记录日志了。。。");
    }

    // 通过调用被注解的方法获取切入点表达式
    @AfterReturning("pt1()")
    public void printLogAfterReturning(){
        System.out.println("后置通知Logger类中的printLogAfterReturning方法开始记录日志了。。。");
    }
    
    // 通过调用被注解的方法获取切入点表达式
	@AfterThrowing("pt1()")
    public void printLogAfterThrowing(){
        System.out.println("异常通知Logger类中的printLogAfterThrowing方法开始记录日志了。。。");
    }
}

纯注解配置AOP

在Spring配置类前添加@EnableAspectJAutoProxy注解,可以使用纯注解方式配置AOP

@Configuration
@ComponentScan(basePackages="cn.maoritian")
@EnableAspectJAutoProxy			// 允许AOP
public class SpringConfiguration {
    // 具体配置
    //...
}

使用注解配置AOP的bug

在使用注解配置AOP时,会出现一个bug. 四个通知的调用顺序依次是:前置通知,最终通知,后置通知. 这会导致一些资源在执行最终通知时提前被释放掉了,而执行后置通知时就会出错.

猜你喜欢

转载自www.cnblogs.com/kyrielin/p/13172075.html