第六章 AOP编程

target

掌握AOP概念和相关术语
会编写基于XML的AOP编程和基于注解的AOP编程
理解MethodBeforeAdvice接口和execution表达式的使用
理解AOP底层原理
了解AOP中的一个"坑"

1. AOP概述

AOP (Aspect Oriented Programing) ⾯向切⾯编程 ,aop编程本质上就是Spring动态代理开发

以切⾯为基本单位的程序开发,通过切⾯间的彼此协同,相互调⽤,完成程序的构建

切⾯ = 切⼊点 + 额外功能

OOP (Object Oritened Programing) ⾯向对象编程 ,比如:Java

以对象为基本单位的程序开发,通过对象间的彼此协同,相互调⽤,完成程序的构建

POP (Producer Oriented Programing) ⾯向过程(⽅法、函数)编程 ,比如:C语言

以过程为基本单位的程序开发,通过过程间的彼此协同,相互调⽤,完成程序的构建

举个例子来说明一下:

如果有一个UserService类,里面有这样三个方法:addUser()、updateUser()、deleteUser()。现在希望给这三个方法都加上事务,但是不能修改这个类的源码。所以我们想到写一个子类去继承他,然后修改子类就好了。

SonUserService类继承UserService类,子类里可以直接调用父类的方法。比如:addUser()方法我就可以先调用父类的addUser()方法:super.addUser()。然后在前面开启事务,在后面提交事务。

我们发现,后面两个方法依次都要这么做,而且开启事务、提交事务不断重复出现。

这是我们以前写的东西,我们称之为纵向继承。这就是传统的oop思想。


纵向继承

现在有一个新的解决方案:

既然开启事务和提交事务是重复代码,可以把它抽取出来放在一个类里面。

写一个A类,before()方法里面写开启事务,after()方法里面写提交事务。我希望A类的方法都要作用到UserService类的相应方法的前后去,此时就可以用到代理。写一个代理类,把这两个无关的类建立起关系,就像房产中介就是代理类,他把买房子的和买房子的建立起关系。代理类把左边的代码拿过来,把右边的代码也拿过来,然后组合在一起。所以代理类这样写:

A.before();
UserService.addUser();
A.after();

有人可能说,你这么写完和刚才有啥区别?不是一样了吗?

不一样的。Spring提供了动态代理,代理类其实是Spring去做的,我们只需要将A类写好,UserService类写好,然后都交给Spring去做了,这样我们做的事就仅仅是业务逻辑了,该有的功能都有,但是代码量却少了。这就说我们即将学习的AOP。

AOP有如下特点和应用:

  • 经典应用:性能检测、事务管理、安全检查、缓存。

  • Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码。

1.1 AOP定义

AOP的概念: 本质就是Spring的动态代理开发,通过代理类为原始类增加额外功能。 好处: 利于原始类的维护,减少代码量,使开发人员专注于业务逻辑的开发。 注意: AOP编程不可能取代OOP,OOP编程有益补充。

1.2 AOP术语

AOP术语
  • target:目标类,需要被代理的类。也就是上图的UserService类。

  • JointPoint:连接点,指可能被拦截到的方法。例如:所有的方法。

  • PointCut:切入点,已经被增强的方法。例如:addUser()

    怎么记连接点和切入点:

    比如:洗手间所有的马桶就是连接点,正在被使用的马桶就是切入点

  • advice:通知/增强,增强的代码。例如:before()、after()

  • Weaving:织入,把增强advice应用到目标对象target来创建新的代理对象Proxy的过程。

  • Proxy:代理类

  • aspect:切面,切入点和通知的结合。

1.3 4种advice

如果要对一个切入点进行增强,首先考虑的应该是在切入点的什么位置进行增强,是在切入点前面增强,还是后面,还是前后一起?

通知类型 需要实现的接口 接口中的方法 执行时机
前置通知 org.springframework.aop.MethodBeforeAdvice before() 目标方法之前
后置通知 org.springframework.aop.AfterReturningAdvice afterReturning() 目标方法执行后
异常通知 org.springframework.aop.ThrowsAdvice 目标方法发生异常时
环绕通知 org.aopalliance.intercept.MethodInterceptor invoke() 调用目标方法的整个过程

2. 基于xml的AOP编程

新建Java项目:Spring-06

2.1 前置通知

前置通知需要实现MethodBeforeAdvice接口。

① 代码实现

第一步:导入jar

  • aopalliance.jar

  • aspectjweaver.jar

  • commons-logging-1.2.jar

  • spring-aop-4.3.9.RELEASE.jar

  • spring-beans-4.3.9.RELEASE.jar

  • spring-context-4.3.9.RELEASE.jar

  • spring-core-4.3.9.RELEASE.jar

  • spring-expression-4.3.9.RELEASE.jar

注意:必须有 aopalliance 和 aspectjweaver 的支持,否则 aop功能无法实现。

第二步:编写目标类

  • UserService.java
package com.lee.spring.service;

public interface UserService {
    void addUser();
    void updateUser();
    boolean deleteUser(int no);
}
  • UserServiceImpl.java
package com.lee.spring.service.impl;

public class UserServiceImpl implements UserService {

    @Override
    public void addUser() {
        System.out.println("addUser...");
    }

    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...");
    return true;
    }

}

第三步:编写通知类BeforeAdvice.java

package com.lee.spring.aop;

public class BeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("前置通知执行了。。。。");
    }
    
}

第四步:编写配置文件

<?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-4.3.xsd">

    <!-- 将目标类纳入到IOC容器 -->
    <bean id="userService" class="com.lee.spring.service.impl.UserServiceImpl"></bean>

    <!-- 将通知类纳入到IOC容器 -->
    <bean id="beforeAdvice" class="com.lee.spring.aop.BeforeAdvice"></bean>
    
    <!-- 配置aop -->
    <aop:config>
        <!-- 配置切入点 -->
        <aop:pointcut expression="execution(void com.lee.spring.service.impl.UserServiceImpl.addUser())"  id="pointcut"/>
        <!-- 将切入点和通知连接起来 -->
        <!-- advisor就是顾问的意思,顾问就是给两个点起一个桥梁作用 -->
        <aop:advisor advice-ref="beforeAdvice" pointcut-ref="pointcut"/>
    </aop:config>
</beans>

第五步:测试

@Test
public void test01() {
  //1.加载配置文件
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  //2. 获取bean
  UserService userService  = (UserService)context.getBean("userService");
  userService.addUser();
  userService.deleteUser(1);
  userService.updateUser();
}

输出:

前置通知执行了。。。。
addUser...
deleteUser...
updateUser...

可以通过 debug的方式来查看 addUser()方法、deleteUser()、updateUser()方法:


Debug As 进行启动:


可以发现,userService对象使用的就是代理对象。

扩展:

如果我们想给delete方法也配置一个前置通知,直接修改配置文件就可以:

<!-- 配置aop -->
<aop:config>
  <!-- 配置切入点 -->
  <aop:pointcut expression="execution(void com.lee.spring.service.UserServiceImpl.impl.addUser()) or execution( boolean com.lee.spring.service.UserServiceImpl.impl.deleteUser(int))"  id="pointcut"/>
  <!-- 将切入点和通知连接起来 -->
  <!-- advisor就是顾问的意思,顾问就是给两个点起一个桥梁作用 -->
  <aop:advisor advice-ref="beforeAdvice" pointcut-ref="pointcut"/>
</aop:config>

② MethodBeforeAdvice详解

MethodBeforeAdvice接⼝作⽤:额外功能运⾏在原始⽅法执⾏之前,进⾏额外功能操作。

关于前置通知类BeforeAdvice中before()方法中的参数解释

public class BeforeAdvice implements MethodBeforeAdvice {

  /**
        Method: 额外功能所增加给的那个原始⽅法
                比如:login⽅法、register⽅法、showOrder⽅法
        Object[]: 额外功能所增加给的那个原始⽅法的参数。
                比如:String name,String password、User
        Object: 额外功能所增加给的那个原始对象 
                比如:UserServiceImpl、OrderServiceImpl
  */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        
        System.out.println("method名字:" + method.getName());
        System.out.println("method参数个数:" + method.getParameterCount());
        
        System.out.println( "args:" + Arrays.toString(args));
        
        System.out.println("target:" + target);
        
        System.out.println("前置通知执行了。。。。");
    }
    
}

输出:

method名字:deleteUser
method参数个数:1
method的返回值类型:boolean
args:[1]
target:com.lee.service.UserServiceImpl@6ab7a896
前置通知执行了。。。。

Method: 切入点(目标方法)

Object[] args:原始方法的参数

Object target:原始对象或目标对象(UserServiceImpl对象)

③ execution详解

例: execution (* com.sample.service..*.*(..))

整个表达式可以分为五个部分:

1、execution():表达式主体。

2、第一个*号:表示返回类型, *号表示所有的类型。

3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service包、子孙包下所有类的方法。

4、第二个*号:表示类名,*号表示所有的类。

5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数

🌰:

  • boolean addStudent(com.lee.entity.Student)

    返回值类型为boolean,参数类型为com.lee.entity.Student的所有叫addStudent()方法。

  • boolean com.lee.service.StudentService.addStudent(com.lee.entity.Student)

    返回值为boolean,参数类型为com.lee.entity.Student,在com.lee.service.StudentService类下的addStudent方法。

  • * addStudent(com.lee.entity.Student)

    返回值任意,参数类型为com.lee.entity.Student的所有叫addStudent()方法。

  • void *(com.lee.entity.Student)

    返回值为void,参数类型为com.lee.entity.Student的任意方法。

  • * com.lee.service.*.*(..)

    返回值任意,com.lee.service包下的所有类下的所有方法,参数类型任意。(不包含子包)

  • * com.lee.service..*.*(..)

    返回值任意,com.lee.service包下的所有类下的所有方法,参数类型任意。(包含子包)

2.2 后置通知

后置通知需要实现AfterReturningAdvice接口。

第一步:导入jar

  • aopalliance.jar

  • aspectjweaver.jar

第二步:编写目标类

  • UserService.java
public interface UserService {
    void addUser();
    void updateUser();
    boolean deleteUser(int no);
}
  • UserServiceImpl.java
public class UserServiceImpl implements UserService {

    @Override
    public void addUser() {
        System.out.println("addUser...");
    }

    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...");
        return true;
    }

}

第三步:编写通知类AfterAdvice.java

public class AfterAdvice implements AfterReturningAdvice {

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("返回值:" + returnValue);
        
        System.out.println("method方法名:" + method.getName());
        System.out.println("method参数个数:" + method.getParameterCount());
        System.out.println("method返回值类型:" + method.getReturnType());
        
        System.out.println( "目标对象:" +target);
        
        System.out.println("后置通知执行。。。");
    }

}

第四步:编写配置文件

<?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"
    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.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- 将目标类纳入到IOC容器 -->
    <bean id="userService" class="com.lee.service.UserServiceImpl"></bean>

    <!-- 将通知类纳入到IOC容器 -->
    <bean id="afterAdvice" class="com.lee.aop.AfterAdvice"></bean>
    
    <!-- 配置aop -->
    <aop:config>
        <!-- 配置切入点(目标方法) -->
        <aop:pointcut expression="execution(* com.lee.spring.service.UserServiceImpl.impl.updateUser()) or execution(* com.lee.spring.service.UserServiceImpl.impl.deleteUser(int))" id="pointcut"/>
        <!-- 配置顾问(将目标方法和通知连接) -->
        <aop:advisor advice-ref="afterAdvice" pointcut-ref="pointcut"/>
    </aop:config>
    
</beans>

第五步:测试

@Test
    public void test02() {
        //1.加载配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2. 获取bean
        UserService userService  = (UserService)context.getBean("userService");
        userService.addUser();
        System.out.println("-----------");
        userService.deleteUser(1);
        System.out.println("-------------");
        userService.updateUser();
    }

输出:

addUser...
-----------
deleteUser...
返回值:true
method方法名:deleteUser
method参数个数:1
method返回值类型:boolean
目标对象:com.lee.service.UserServiceImpl@fdefd3f
后置通知执行。。。
-------------
updateUser...
返回值:null
method方法名:updateUser
method参数个数:0
method返回值类型:void
目标对象:com.lee.service.UserServiceImpl@fdefd3f
后置通知执行。。。

2.3 异常通知

异常通知需要实现ThrowsAdvice接口。

第一步:导入jar

  • aopalliance.jar

  • aspectjweaver.jar

第二步:编写目标类

  • UserService.java
public interface UserService {
    void addUser();
    void updateUser();
    boolean deleteUser(int no);
}
  • UserServiceImpl.java
public class UserServiceImpl implements UserService {

    @Override
    public void addUser() {
        System.out.println("addUser...");
    }

    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...");
        return true;
    }

}

第三步:编写通知类

public class ExceptionAdvice implements ThrowsAdvice {

    public void afterThrowing(Method method, Object[] args, Object target, Exception ex){
        
        System.out.println("method方法名:" + method.getName());
        System.out.println("method方法参数个数:" + method.getParameterCount());
        System.out.println("method方法默认值:" + method.getDefaultValue());
        
        System.out.println("args" +Arrays.toString( args));
        
        System.out.println("目标对象:" + target);
        
        System.out.println("异常信息:" + ex.getMessage());
        
    }

}

我们发现ThrowsAdvice接口中没有声明方法,但是文档中有提示,告诉我们必须写一个这样的方法:


第四步:编写配置文件

<?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"
    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.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- 将目标类纳入到IOC容器 -->
    <bean id="userService" class="com.lee.service.UserServiceImpl"></bean>

    <!-- 将通知类纳入到IOC容器 -->
    <bean id="excetionAdvice" class="com.lee.aop.ExceptionAdvice"></bean>
    
    <!-- 配置aop -->
    <aop:config>
        <!-- 配置切入点(目标方法) -->
        <aop:pointcut expression="execution(* com.lee.service.UserServiceImpl.updateUser()) or execution(* com.lee.service.UserServiceImpl.deleteUser(int))" id="pointcut"/>
        <!-- 配置顾问(将目标方法和通知连接) -->
        <aop:advisor advice-ref="excetionAdvice" pointcut-ref="pointcut"/>
    </aop:config>
    
</beans>

我们将updateUser和deleteUser方法配置了增强。

第五步:测试

@Test
public void testBeforeAdvice() {
  //1.加载配置文件
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  //2. 获取bean
  UserService userService  = (UserService)context.getBean("userService");
  userService.addUser();
  System.out.println("-----------");
  userService.deleteUser(1);
  System.out.println("-------------");
  userService.updateUser();
}

输出:

addUser...
-----------
deleteUser...
-------------
updateUser...

发现,增强没有起作用。这是由于切入点(目标方法)没有异常,所以异常通知不会触发。

修改目标方法:给UserServiceImpl类中deleteUser()方法制造一个异常

@Override
public boolean deleteUser(int no) {
  int i = 1/0;
  System.out.println("deleteUser...");
  return true;
}

输出:

addUser...
-----------
method方法名:deleteUser
method方法参数个数:1
method方法默认值:null
args[1]
目标对象:com.lee.service.UserServiceImpl@11dc3715
异常信息:/ by zero

发现,异常通知触发,而且程序也抛出了异常,并且终止在发生异常的代码处,后面的代码不会继续执行。

2.4 环绕通知

看名字就知道环绕通知是环绕着目标方法的,既然是环绕,所有他能实现前置通知、后置通知和异常通知。

环绕通知需要实现MethodInterceptor接口。

环绕通知的本质是拦截器。

下面看一下案例实现:

第一步:导入jar

  • aopalliance.jar

  • aspectjweaver.jar

第二步:编写目标类

  • UserService.java
public interface UserService {
    void addUser();
    void updateUser();
    boolean deleteUser(int no);
}
  • UserServiceImpl.java
public class UserServiceImpl implements UserService {

    @Override
    public void addUser() {
        System.out.println("addUser...");
    }

    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...");
        return true;
    }

}

第三步:编写通知类RoundAdvice.java

public class RoundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        /*
         * invocation里包含着目标方法的全部信息
         * 包括:方法名、参数个数、参数值、返回值。。。。
         * 甚至可以控制目标方法是否执行
         */
        Object result = null;
        try {
            //1.invocation.proceed()之前的代码是前置通知
            System.out.println("环绕通知实现的前置通知。。。");
            System.out.println("目标方法名:" + invocation.getMethod().getName());
            result = invocation.proceed();//控制目标方法的执行
            //2.invocation.proceed()之后的代码是后置通知
            System.out.println("环绕通知实现的后置通知。。。");
            System.out.println("目标对象:" + invocation.getThis());
            System.out.println("返回值:" + result);
        }catch (Exception e) {
            //3.异常通知
            System.out.println("环绕通知实现的异常通知。。。");
        }
        /*
         * 返回值是目标方法的返回值
         * 由于环绕通知拥有目标方法的绝对控制权,他甚至可以偷梁换柱:将目标方法的返回值进行更改
         */
        return result;
    }

}

第四步:编写配置文件

<?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"
    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.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- 将目标类纳入到IOC容器 -->
    <bean id="userService" class="com.lee.service.UserServiceImpl"></bean>

    <!-- 将通知类纳入到IOC容器 -->
    <bean id="roundAdvice" class="com.lee.aop.RoundAdvice"></bean>
    
    <!-- 配置aop -->
    <aop:config>
        <!-- 配置切入点(目标方法) -->
        <aop:pointcut expression="execution(* com.lee.service.UserServiceImpl.deleteUser(int))" id="pointcut"/>
        <!-- 配置顾问(将目标方法和通知连接) -->
        <aop:advisor advice-ref="roundAdvice" pointcut-ref="pointcut"/>
    </aop:config>
    
</beans>

第五步:测试

@Test
public void test04() {
  //1.加载配置文件
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  //2. 获取bean
  UserService userService  = (UserService)context.getBean("userService");
  userService.addUser();
  System.out.println("-----------");
  userService.deleteUser(1);
  System.out.println("-------------");
  userService.updateUser();
}

输出:

addUser...
-----------
环绕通知实现的前置通知。。。
目标方法名:deleteUser
deleteUser...
环绕通知实现的后置通知。。。
目标对象:com.lee.service.UserServiceImpl@1a0dcaa
返回值:true
-------------
updateUser...

如果想查看环绕通知实现的异常通知,将目标方法设计一个异常就可以了。

3. 基于注解的aop

将一个类变成有特定功能的类,有四种做法:

  • 继承类

  • 实现接口

  • 注解

  • 配置文件

下面我们说一下用注解方式实现的的aop:

3.1 前置通知

① 实现步骤

第一步:导入jar包

  • aspectjweaver.jar

第二步:编写业务类

  • UserService.java
public interface UserService {
    void addUser(User student);
    void updateUser(User student);
    boolean deleteUser(int no);
}
  • UserServiceImpl
@Service("userService")
public class UserServiceImpl implements UserService {

    @Override
    public void addUser(User user) {
        System.out.println("addUser..." + user);
    }

    @Override
    public void updateUser(User user) {
        System.out.println("updateUser..." + user);
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...学号是:" + no );
        return true;
    }
}

第三步:编写通知类AnnotationAdvice.java

@Component
@Aspect
public class AnnotationAdvice {

  @Before("execution(* com.lee.service.*.addUser(..))")
  public void before() {
    System.out.println("前置通知执行。。。");
  }
}

在类上加注解@Aspect,就表示把普通的类变成了通知类。

在方法上加注解@Before,就表示把普通的方法变成了前置方法。@Before里面需要些切入点的表达式。

第四步:编写配置文件

<?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"
    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.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- 开启包扫描 -->
    <context:component-scan base-package="com.lee"></context:component-scan>
    
    <!-- 开启aop支持 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    
</beans>

如果要用注解开发aop,配置文件里必须开启对aop的支持。

第五步:测试

@Test
public void testBeforeAdvice() {
  //1.加载配置文件
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  //2. 获取bean
  UserService userService  = (UserService)context.getBean("userService");
  User user = new User(1001, "zs");
  userService.addUser(user);
  System.out.println("-----------");
  userService.deleteUser(1);
  System.out.println("-------------");
  userService.updateUser(user);
}

输出:

前置通知执行。。。
addUser...User [no=1001, name=zs]
-----------
deleteUser...学号是:1
-------------
updateUser...User [no=1001, name=zs]

② 注意事项

  • 普通的类要变成通知类,需要在类上加注解@Aspect

  • 通知类里面的方法要变成前置通知方法,需要在方法上@Before注解,而且注解里面需要配置切入点表达式。

  • 如果想让注解配置的aop生效,需要在配置文件中开启对aop的支持。

  • 此案例我们将目标类和通知类加入到IOC容器的方式是:注解扫描方式。也可以用xml配置方式。

③ JoinPoint

怎么可以像实现接口那样,获取到一些关于切入点的相关信息呢?用JoinPoint。

注意是org.aspectj.lang.JoinPoint。


通知类:

@Component
@Aspect
public class AnnotationAdvice {

    @Before("execution(* com.lee.service.*.addUser(..))")
    public void before(JoinPoint jp) {
        
    System.out.println("目标方法相关信息:" + jp.getSignature());
        System.out.println("目标方法名:" + jp.getSignature().getName());
        System.out.println( "方法参数:" + Arrays.toString(jp.getArgs()));
        System.out.println("目标对象:" + jp.getThis());
        
        System.out.println("前置通知执行。。。");
    }
}

输出:

目标方法签名:void com.lee.service.UserService.addUser(User)
目标方法名:addUser
方法参数:[User [no=1001, name=zs]]
目标对象:com.lee.service.UserServiceImpl@7c7b252e
前置通知执行。。。
addUser...User [no=1001, name=zs]
-----------
deleteUser...学号是:1
-------------
updateUser...User [no=1001, name=zs]

④ 相关方法

方法 解释
getSignature() 获取目标方法相关信息
getSignature().getName() 获取目标方法名
getArgs() 获取目标方法参数
getThis() 获取目标对象
getTarget() 获取目标对象

3.2 后置通知

后置通知的注解是@AfterReturning

@Component
@Aspect
public class AnnotationAdvice {
    
  @AfterReturning( pointcut  = "execution(* com.lee.service.*.deleteUser(..))" , returning = "returnVal")
  public void after(JoinPoint jp , Object returnVal) {

    System.out.println("目标方法相关信息:" + jp.getSignature());
    System.out.println("目标方法名:" + jp.getSignature().getName());
    System.out.println( "方法参数:" + Arrays.toString(jp.getArgs()));
    System.out.println("目标对象:" + jp.getThis());

    System.out.println("返回值:" + returnVal);

    System.out.println("后置通知执行。。。");
  }
}

注意:

  • 如果想查看返回值的回话,需要在@AfterReturning里声明returning = "xxx"。

  • @AfterReturning( pointcut = "execution(* com.lee.service.*.deleteUser(..))" , returning = "returnVal")中,既可以用 pointcut = "xxx",也可以用 value = "xxx"。

3.3 异常通知

异常通知用注解@AfterThrowing。

如果需要打印异常信息需要在@AfterThrowing里加一个throwing = "xxx"

@Component
@Aspect
public class AnnotationAdvice {

    @AfterThrowing(pointcut = "execution(* com.lee.service.*.addUser(..))" , throwing = "th")
    public void myException(JoinPoint jp , Exception th) {
        
        System.out.println("目标方法相关信息:" + jp.getSignature());
        System.out.println("目标方法名:" + jp.getSignature().getName());
        System.out.println( "方法参数:" + Arrays.toString(jp.getArgs()));
        System.out.println("目标对象:" + jp.getThis());

        System.out.println("异常信息:" + th.getMessage());
        
        System.out.println("异常通知执行。。。");
    }
}

输出:

目标方法相关信息:void com.lee.service.UserService.addUser(User)
目标方法名:addUser
方法参数:[User [no=1001, name=zs]]
目标对象:com.lee.service.UserServiceImpl@48f2bd5b
异常信息:/ by zero
异常通知执行。。。

扩展:

上面方法捕获的异常级别是Exception,如果我只想捕获特定异常,比如:ArithmeticException。当程序产生其他异常时,就会捕捉不到。

@Aspect
public class AnnotationAdvice {

    @AfterThrowing(pointcut = "execution(* com.lee.service.*.addUser(..))" , throwing = "th")
    public void myException(JoinPoint jp , NullPointerException th) {
        
        System.out.println("目标方法相关信息:" + jp.getSignature());
        System.out.println("目标方法名:" + jp.getSignature().getName());
        System.out.println( "方法参数:" + Arrays.toString(jp.getArgs()));
        System.out.println("目标对象:" + jp.getThis());

        System.out.println("异常信息:" + th.getMessage());
        
        System.out.println("异常通知执行。。。");
    }
}

异常信息就没有被捕获到,是由于程序中异常通知仅仅捕获的是NullPointerException,而目标方法里面出现的异常时ArithmeticException,所以没有捕获到

3.4 最终通知

最终通知是无论程序正常执行还是异常执行,都会执行的一个通知。类似于try、catch、finally里面的finally。

最终通知用的注解是@After。

@Component
@Aspect
public class AnnotationAdvice {
    
    @After("execution(* com.lee.service.*.addUser(..))" )
    public void zuizhong() {
        System.out.println("最终通知执行。。。");
    }
}

3.5 环绕通知

环绕通知用的注解是@Around

@Component
@Aspect
public class AnnotationAdvice {

    @Around(value = "execution(* com.lee.service.*.addUser(..))")
  //获取目标对象的详细信息不能再用JoinPoint,需要用子接口ProceedingJoinPoint。
    public void around(ProceedingJoinPoint jp) {
        try {
            //执行目标方法之前,前置通知
            System.out.println("前置通知执行。。。");
            jp.proceed();//执行目标方法
            //执行目标方法之后,后置通知
            System.out.println("后置通知执行。。。");
        }catch (Throwable e) {
            System.out.println("异常通知执行。。。");
        }finally {
            System.out.println("最终通知执行。。。");
        }
        
    }
}

目标方法没有异常,输出:

前置通知执行。。。
addUser...User [no=1001, name=zs]
后置通知执行。。。
最终通知执行。。。

目标方法存在异常,输出:

前置通知执行。。。
addUser...User [no=1001, name=zs]
异常通知执行。。。
最终通知执行。。。

执行顺序

  • 目标方法没有异常:

    前置通知-->目标方法-->后置通知-->最终通知

  • 目标方法存在异常:

    前置通知-->目标方法-->异常通知-->最终通知

注意事项

  • 获取目标对象的详细信息不能再用JoinPoint,需要用子接口ProceedingJoinPoint。

  • catch块里异常信息的捕获级别必须是Throwable,不能是其他。

  • 如果目标方法没有返回值,则环绕通知方法的返回值也可以是void。如果目标方法有返回值,则环绕通知方法也必须有返回值,否则会报错:Null return value from advice does not match primitive return type for: ....

  @Aspect
public class AnnotationAdvice {

    @Around(value = "execution(* com.lee.service.*.deleteUser(..))")
    public Object around(ProceedingJoinPoint jp) {
        Object result = null ;
        try {
            //执行目标方法之前,前置通知
            System.out.println("前置通知执行。。。");
            result =  jp.proceed();//执行目标方法
            //执行目标方法之后,后置通知
            System.out.println("后置通知执行。。。");
        }catch (Throwable e) {
            System.out.println("异常通知执行。。。");
        }finally {
            System.out.println("最终通知执行。。。");
        }
        return result;
    }
}

4. AOP底层实现原理

AOP的底层原理实现就是Spring的动态代理。

4.1 Spring创建的动态代理类在哪⾥

Spring框架在运⾏时,通过动态字节码技术,在JVM中创建的,运⾏在JVM内部,等程序结束后,会和 JVM ⼀起消失。

  • 什么叫动态字节码技术:


    动态字节码技术

JVM是怎么创建对象的?

首先需要编写 .java 的源文件,源文件编译后会变成 .class的字节码文件,JVM通过加载字节码就能创建出这个类的对象。

那什么是动态字节码?

动态字节码就意味着不需要编写 .java的源文件,也就不能编译为 .class文件。但是虚拟机创建对象肯定需要的还是 .class格式的字节码,此时的字节码就是动态创建的。

动态字节码是怎么创建的?

通过第三方的动态字节码框架创建,常见的动态字节码框架有 ASM、Javaassist、Cglib。这些框架可以动态的生成字节码。

所以,动态代理类的本质就是动态字节码技术,比如 UserServiceProxy就会自动的通过动态字节码在JVM中生成,而不需要去手动编写。

  • 结论:

    动态代理不需要定义类⽂件,都是JVM运⾏过程中动态创建的,所以不会造成静态代理,类⽂件数量过多,影响项⽬管理的问题。

4.2 Spring工厂如何加工原始对象

  • 思路分析:

就是使用后置bean去对原始对象进行再加工,再加工阶段加上额外功能。

再加工的过程使用的是动态代理。


Spring工厂创建对象.png
  • 核心编码

UserService.java接口:

package com.lee.factory;

public interface UserService {

    void register();
    void login();
}

UserServiceImpl.java 实现类:

package com.lee.factory;

public class UserServiceImpl implements UserService {

    @Override
    public void register() {
        System.out.println("注册。。。。。");
    }

    @Override
    public void login() {
        System.out.println("登录。。。。");
    }

}

将UserServiceImpl纳入到IoC容器:

<bean id="userService" class="com.lee.factory.UserServiceImpl"></bean>

创建ProxyBeanPostProcessor.java,实现代理:

package com.lee.factory;

public class ProxyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String args) throws BeansException {
        
        ClassLoader loader = ProxyBeanPostProcessor.class.getClassLoader();
        Class<?>[] interfaces = bean.getClass().getInterfaces();
        InvocationHandler invocationHandle = new InvocationHandler() {
            
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("---------log.....-----");
                Object invoke = method.invoke(bean, args);
                return invoke;
            }
        };
        //使用JDK代理创建对象
        Object ret = Proxy.newProxyInstance(loader, interfaces, invocationHandle);
        return ret;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String args) throws BeansException {
        return bean;
    }

}

测试:

public class TestAopProxy {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext-factory.xml");
        UserService userService = (UserService)context.getBean("userService");
        userService.register();
        userService.login();
    }
}

输出:

---------log.....-----
注册。。。。。
---------log.....-----
登录。。。。

5. AOP开发中的⼀个坑

先看一个案例:

业务里有两个方法,分别是a() 和 b():

package com.lee.spring.service.impl;

public class UserServiceImpl {

    public void a() {
        System.out.println("a方法执行");
    }
    
    public void b() {
        System.out.println("b方法执行");
    }
}

有一个额外方法,通过 Spring 进行增强:

package com.lee.spring.aop;

public class Before implements MethodBeforeAdvice {

    @Override
    public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
        System.out.println("前置增强执行");
    }   
}

配置文件:

<?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-4.3.xsd">
    
    <!-- 将UserService纳入到IOC容器 -->
    <bean id = "userService" class="com.lee.spring.service.impl.UserServiceImpl"></bean>
    
    <!-- 将Before增强纳入到IOC容器 -->
    <bean id = "before" class="com.lee.spring.aop.Before"></bean>

    <!-- 配置增强 -->
    <aop:config>
        <aop:pointcut expression="execution(* *..UserServiceImpl.*(..))" id="pointcut"/>
        <aop:advisor advice-ref="before" pointcut-ref="pointcut"/>
    </aop:config>
</beans>

编写测试类进行测试:

@Test
public void test01() {
  ApplicationContext context =  new ClassPathXmlApplicationContext("applicationContext.xml");
  UserServiceImpl userService =  (UserServiceImpl)context.getBean("userService");
  userService.a();
  userService.b();
}

控制台输出:

前置增强执行
a方法执行
前置增强执行
b方法执行

以上操作都是正常的 aop 前置增强,如果业务逻辑变为:

package com.lee.spring.service.impl;

public class UserServiceImpl {

    public void a() {
        System.out.println("a方法执行");
        b();
    }
    
    public void b() {
        System.out.println("b方法执行");
    }
}

测试变为:

@Test
public void test01() {
  ApplicationContext context =  new ClassPathXmlApplicationContext("applicationContext.xml");
  UserServiceImpl userService =  (UserServiceImpl)context.getBean("userService");
  userService.a();
}

控制台输出:

前置增强执行
a方法执行
b方法执行

很明显,在方法a() 中 调用b()方法,b()方法的前置增强就不生效了。这就是aop中的一个坑。

原因分析:

在执行 userService.a();时,a()方法调用的时候userService对象,而userService对象是从ioc容器中获取的代理对象,所以可以进行增强。但是在 a() 方法中调用 b() 方法,b() 方法是来自与 userService对象本身,而不是 Spring代理的,所以不具备增强功能。

所以可以这样改进:

package com.lee.spring.service.impl;

public class UserServiceImpl {

    public void a() {
        System.out.println("a方法执行");
        ApplicationContext context =  new ClassPathXmlApplicationContext("applicationContext.xml");
        UserServiceImpl userService =  (UserServiceImpl)context.getBean("userService");
        userService.b();
    }
    
    public void b() {
        System.out.println("b方法执行");
    }
}

在调用 b() 方法的时候,不是直接调用,而是从ioc中获取代理对象,然后调用代理对象的 b() 方法。

但是这样有个问题:Spring工厂是重量级资源,会侵占内存,所以一个应用中只创建一个工厂就足够了。

可以这样更改:

package com.lee.spring.service.impl;

public class UserServiceImpl implements ApplicationContextAware {

    private ApplicationContext context;
    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        this.context = context;
    }
    public void a() {
        System.out.println("a方法执行");
        UserServiceImpl userService =  (UserServiceImpl)context.getBean("userService");
        userService.b();
    }
    
    public void b() {
        System.out.println("b方法执行");
    }
}

6. AOP阶段知识总结

AOP相关知识总结.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容