Spring AOP面向切面编程

2022-11-22 08:26:19 浏览数 (1)

Spring AOP面向切面编程

  • 一.初识AOP
    • 1.介绍AOP
    • 2.初识Spring AOP
  • 二.AOP相关概念
    • 1.AOP关键概念
    • 2.JoinPiont核心方法
    • 3.PointCut切点表达式
  • 三.AOP通知
    • 1.五种通知类型
    • 2.环绕通知
  • 四.基于注解配置Spring AOP
  • 五.AOP中的代理模式的应用
    • 1.Spring AOP的实现原理
    • 2.静态代理
    • 3.AOP底层原理---JDK动态代理
    • 4.CGLib实现代理类

从本节开始,要进入了一个全新的阶段,来学习Spring的AOP面向切面编程。

一.初识AOP

1.介绍AOP

spring提供了一种可插拔的组件技术。听起来很高大上,但在我们日常生活中经常遇到这样的场景,比如说我们现在开发了两个软件模块,A和B,假设软件模块A是系统的用户管理模块,而软件模块B是系统的员工管理模块。这两个模块都拥有自己的业务处理类,他们执行的过程也是以上到下依次执行的。现在我对这两个模块提出一个要求,这两个模块从上到下进行业务处理的过程中,我希望都要进行权限过滤,只有拥有权限的用户才可以访问对应的模块。你可能会在运行实际代码前去增加相应的权限判断的业务代码,A模块加一个,B模块加一个,这样做固然没问题。但是有一天,项目经理说我们现在不需要这两块功能了,那该怎么办呢?此时你又该打开它对应的代码,把所有的权限控制代码全都去掉。那在这时候,有没有更好的办法呢?答案是肯定的。Spring AOP面向切面编程就可以很好地解决这个问题。

所谓面向切面编程就是指在我们的软件运行过程中在执行前,或者执行后,都可以去额外地增加相应的扩展功能。而这个扩展功能我们称之为切面。

就拿当前的例子来说,对于软件模块A和软件模块B,我们在实际的代码运行前,先进入到权限切面中,对权限进行判断。所以我们可以称它为权限切面。权限切面对系统用户的权限进行判断,如果某系统用户拥有访问模块A,或者访问模块B的权限,那就依次向下执行。如果这个用户没有访问权限的话,则由这个切面将其挡在外边。这个权限切面就起到了对应用程序执行前进行拦截的作用。那么随着程序的运行,在程序运行完了以后,我们又可以再额外增加一个日志切面。日志切面的作用是对当前软件运行过程中几点几分运行的参数是什么,输出的结果是什么进行记录。方便我们程序的调试和跟踪。在这里,无论是权限切面,还是日志切面,对于这两个软件模块来说都是额外的,这两个软件模块在运行时也不会感知到有这两个切面的存在。那与此同时,如果有一天我们系统的业务逻辑发生了变化,不需要权限切面和日志切面了,那也只需要在配置文件中进行简单的调整就可以迅速地将这两个切面从当前系统中移除。经过我刚才的描述,是不是有点像我们浏览器中安装的各种各样的插件啊。我们无论使用什么浏览器,这些浏览器都支持插件技术。

Spring AOP 即(Aspect Oriented Programming)面向切面编程。AOP的做法是将通用的、与业务无关的功能抽象封装为切面类。切面可以配置在目标方法的执行前或执行后,真正的做到即插即用。其最终目的是在不修改源代码的情况下对程序行为进行扩展。

2.初识Spring AOP

本节通过案例,一步一步地完成一个AOP的项目配置,首先从感性上了解一下AOP到底能给我们带来哪些功能。

首先创建一个Maven项目,然后创建dao包和service包和aop包,然后创建dao类和service类。因为我现在只是学习AOP,还没有到实际开发案例,所以,里面的方法示意性的编写,只为让我们理解AOP。代码如下:

UserDao.java

代码语言:javascript复制
package com.haiexijun.dao;

/**
 *用户表Dao
 */
public class UserDao {
    public void insert(){
        System.out.println("新增用户数据");
    }
}

EmployeeDao.java

代码语言:javascript复制
package com.haiexijun.dao;

/**
 * 员工表Dao
 */
public class EmployeeDao {
    public void insert(){
        System.out.println("新增员工数据");
    }
}

EmployeeService.java

代码语言:javascript复制
package com.haiexijun.service;

import com.haiexijun.dao.EmployeeDao;

/**
 * 员工服务
 */
public class EmployeeService {
    private EmployeeDao employeeDao;
    public void entry(){
        System.out.println("执行员工入职业务逻辑");
        employeeDao.insert();
    }

    public EmployeeDao getEmployeeDao() {
        return employeeDao;
    }

    public void setEmployeeDao(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }
}

UserService.java

代码语言:javascript复制
package com.haiexijun.service;

import com.haiexijun.dao.UserDao;

/**
 * 用户服务
 */
public class UserService {
    private UserDao userDao;

    public void createUser(){
        System.out.println("执行创建用户的业务逻辑");
        userDao.insert();
    }

    public String generateRandomPassword(String type,Integer length){
        System.out.println("按" type "方式生成" length "位随机密码");
        return "abcdeffdasf";
    }

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

这些类再普通不过了,如果我在此时提出一个全新的要求,无论是service的方法还是dao的方法。我希望在执行之前,在控制台中打印出他们各自执行的时间。通过这些时间信息,可以让我了解到在具体的什么什么时段是一天,我们应用程序负载最高的时刻。此时,你可能会想,这还不简单吗?直接在我们的每一个方法上面增加一个sout,然后将当前系统的时间打印出来不就可以了吗?这么做当然没有问题,但你发现了没有,我们系统中的类呢还是很多的,而每个类中又有着大量的方法。即便是你通过这一句话复制粘贴很多次也是一个很麻烦的事情啊?那与此同时,有一天你的项目经理告诉你,我们也不在需要这些时间的输出了。那该怎么办呢?又要打开每一行的代码将其删除吗?这样的工作显然是十分低级的,而且容易出错。那如果,你是一个架构师,又对Spring非常了解的话,完全可以使用Spring AOP这个技术对这些方法运行前,进行进行拦截,打印时间,再去执行方法内部的代码。像这种不用修改源代码,而去对原有程序行为进行扩展的技术就是Spring AOP了。

具体的做法我下面先进行逐步的演示,然后在后面的小节中再针对于每一个细节进行详细的讲解。

先在pom.xml中引入需要的依赖。虽然spring-context这个依赖里面会又aop模块,但是还是要引入另一个依赖aspectjweaer。关于aspectjweaer这个模块的作用,在后面详细讲解。先把它引入进来就行了。这样pom.xml我们就书写好了。

代码语言:javascript复制
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.13</version>
        </dependency>

        <!--aspectjweaer是Spring AOP的底层依赖-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.8.RC2</version>
        </dependency>

    </dependencies>

然后在resources目录下面创建applicationContext.xml,先进行如下配置,这个配置和之前配置有些出入,多了xmlns:aop等: 下面的一些约束在https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#xsd-schemas-aop的10.1就可以找到。

代码语言:javascript复制
<?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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        https://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        https://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

</beans>

然后在当前容器中进行bean的配置

代码语言:javascript复制
<?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"
       xmlns:context="http://www.springframework.org/schema/context"
       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.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="userDao" class="com.haiexijun.dao.UserDao"/>
    <bean id="employeeDao" class="com.haiexijun.dao.EmployeeDao"/>
    <bean id="userService" class="com.haiexijun.service.UserService">
        <property name="userDao" ref="userDao"/>
    </bean>
    <bean id="employeeService" class="com.haiexijun.service.EmployeeService">
        <property name="employeeDao" ref="employeeDao"/>
    </bean>

</beans>

接下来,创建入口类测试一下:

代码语言:javascript复制
package com.haiexijun;

import com.haiexijun.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringApplication {
    public static void main(String[] args) {
        ApplicationContext context=new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        UserService userService= context.getBean("userService", UserService.class);
        userService.createUser();
    }
}

运行后如下图:

在aop包下面新增加一个新的包aspect,里面创建一个MethodAspect的类,这是一个针对于方法的切面。我们在里面定义一个切面方法,打印执行时间。切面方法必须要额外加入一个JoinPoint类型的参数(连接点), 通过连接点可以获取目标类/方法的信息。那什么是目标类、目标方法呢?其实就是我们真正要执行的这个方法就称之为目标方法,而这个目方法隶属于的类就是目标类。在我们程序在输出的时候,肯定是要打印什么时间,哪个类,什么方法在运行。

代码语言:javascript复制
package com.haiexijun.aspect;

import org.aspectj.lang.JoinPoint;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 切面类
 */
public class MethodAspect {
    /**
     * 切面方法,打印执行时间
     * @param joinPoint 切面方法必须要额外加入一个JoinPoint类型的参数(连接点)
     *                  通过连接点可以获取目标类/方法的信息
     */
    public void printExecutionTime(JoinPoint joinPoint){
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        String now= sdf.format(new Date());
        String ClassName= joinPoint.getTarget().getClass().getName();//获取目标类的名称
        String MethodName=joinPoint.getSignature().getName();//获取目标方法的名称
        System.out.println("---->" now ":" ClassName "." MethodName);

    }

}

写好这个类后,spring可不知道printExecutionTime方法是在哪些的哪些方法运行前去执行啊。所以,我们还是要在啊applicationContext里面进行aop进行配置,说明我这个切面类作用的范围是什么。 具体的配置方法:

代码语言:javascript复制
    <!--AOP配置-->
    <bean id="methodAspect" class="com.haiexijun.aspect.MethodAspect"/>
    <aop:config>
        <!--pointcut是切点的意思,使用execution表达式描述切面的作用范围-->
        <!--下面的execution表达式说明切面作用于com.haiexijun包下的所有类的所有方法上-->
        <aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*.*(..))"/>
        <!--定义切面类-->
        <aop:aspect ref="methodAspect">
            <!--before通知Advice,代表在目标方法运行前先执行切面类里面的方法-->
            <aop:before method="printExecutionTime" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

后面具体讲解里面的配置。

我们回到主程序,直接运行:

我们之前关于时间打印的需求就完成了。如果有一天,不需要这个功能了,就在applicationContext里面把这段aop配置信息给注释掉就行了。到这里,相信你对Spring AOP就有一定的了解了,接下来的小节就来深入学习里面的配置了。

二.AOP相关概念

1.AOP关键概念

Spring AOP与AspectJ的关系 Eclipse AspectJ 是一种基于Java平台的面向切面编程的语言。AspectJ 有一套完整的体系,可以在运行时实现AOP面向切面编程。但是作为Spring AOP来说,它并不是完全使用AspectJ来做的。作为Spring AOP使用AspectWeaver实现类与方法匹配。 Spring AOP利用代理模式实现对象运行时的功能拓展。

几个关键的概念:

相关概念

说明

Aspect

切面,具体的可插拔组件功能类,通常一个切面只实现一个通用功能

Target Class/Method

目标类、目标方法,指真正要执行与业务相关的方法

PiontCut

切入点,使用execution表达式说明切面要作用在系统的哪些类上

JoinPiont

连接点,切面运行过程中是否包含了目标类/方法元数据的对象

Advice

通知,说明具体的切面的执行时机,Spring包含了五种不同类型的通知。

2.JoinPiont核心方法

方法

说明

Object getTarget()

获取IoC容器内目标对象

Signature getSignature()

获取目标方法

Object[] getArgs()

获取目标方法的参数

前面2个方法前面已经演示过我,下面就来演示一下getArgs()这个方法的使用。 在切面类的方法内添加如下的代码

代码语言:javascript复制
        Object[] args=joinPoint.getArgs();
        System.out.println("---->参数个数:" args.length);
        for (Object arg:args){
            System.out.println("---->参数:" arg);
        }

这段代码的含义是:获取到调用的方法传入的参数 我们直接运行入口程序,因为userService.createUser();并不用传入参数,所以args的个数位0

我们更改入口的方法调用代码为

代码语言:javascript复制
        userService.generateRandomPassword("MD5",10);

然后运行程序:

3.PointCut切点表达式

public可以不写,因为默认就是public。 如果要让程序匹配Service结尾的类名:

代码语言:javascript复制
<aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*Service.*(..))"/>

public是可以去掉的

代码语言:javascript复制
<aop:pointcut id="pointcut" expression="execution( * com.haiexijun..*Service.*(..))"/>

如果只想捕获create开头的方法:

代码语言:javascript复制
<aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*.create*(..))"/>

如果只想捕获不传入参数的方法,直接把()内的2个点去掉:

代码语言:javascript复制
<aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*.*())"/>

如果只想捕获只有2个参数的方法,用*代表参数:

代码语言:javascript复制
<aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*.*(*,*))"/>

还可以指定方法的参数的类型:

代码语言:javascript复制
<aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*.*(String,*))"/>

在实际开发中对指定的类进行匹配用得比较多。通常不会干涉到参数的类型数量和返回值是什么,采用通配符就好。

三.AOP通知

1.五种通知类型

通知

说明

Before Advice

前置通知,目标方法运行前执行

After Returning Advice

返回后通知,目标方法返回数据后执行

After Throwing

异常通知,目标方法抛出异常后执行

After Advice

后置通知,目标方法运行后执行

Around Advice

环绕通知,最强大的通知,自定义通知执行时机,可决定目标方法是否可运行

还有一个引进的特殊的”通知“—引介增强 这不是Spring官方提供的通知。引介增强(IntroductionInterceptor)是对类的增强,而非方法。他跟通知实际上是没有关系的,它本质是一个拦截器。其他的通知是坐拥在方法上,而引介增强是作用在类上面的。引介增强允许在运行时为目标类增加新属性或方法。 引介增强允许在运行时改变类的行为,让类随运行环境动态变更。引介增强我们了解一下就可以了,日常开发用得比较少。

下面还是把关注点放在这5种通知类型上。

我们在之前编写的切面类中新增加一个方法doAfter() ,看名字就知道了,这是用于后置通知的处理方法。作为后置通知同样要加入JoinPiont连接点参数。作为方法内容我们随便打印一下,了解一下它的执行时机就可以了。

代码语言:javascript复制
    /**
     * 后置通知
     * @param joinPoint
     */
    public void doAfter(JoinPoint joinPoint){
        System.out.println("<----触发后置通知");
    }

然后要在xml中配置一下:

代码语言:javascript复制
    <aop:config>
        <!--pointcut是切点的意思,使用execution表达式描述切面的作用范围-->
        <!--下面的execution表达式说明切面作用于com.haiexijun包下的所有类的所有方法上-->
        <aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*Service.*(..))"/>
        <!--定义切面类-->
        <aop:aspect ref="methodAspect">
            <!--before通知,代表在目标方法运行前先执行切面类里面的方法-->
            <aop:before method="printExecutionTime" pointcut-ref="pointcut"/>
            <!--after通过-->
            <aop:after method="doAfter" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

运行后:

我们来试试返回后通知,目标方法返回数据后执行,返回后通知要传入2个参数第一个是JoinPoint , 第2个是Object 表示目标方法的返回值。

代码语言:javascript复制
    /**
     * 返回后通知
     * @param joinPoint 连接点
     * @param ret 目标方法的返回值
     */
    public void doAfterReturning(JoinPoint joinPoint,Object ret){
        System.out.println("<----返回后通知:" ret);
    }

对于返回后通知,我们的xml的配置也有些不一样。

代码语言:javascript复制
    <aop:config>
        <!--pointcut是切点的意思,使用execution表达式描述切面的作用范围-->
        <!--下面的execution表达式说明切面作用于com.haiexijun包下的所有类的所有方法上-->
        <aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*Service.*(..))"/>
        <!--定义切面类-->
        <aop:aspect ref="methodAspect">
            <!--before通知,代表在目标方法运行前先执行切面类里面的方法-->
            <aop:before method="printExecutionTime" pointcut-ref="pointcut"/>
            <!--after通过-->
            <aop:after method="doAfter" pointcut-ref="pointcut"/>
            <!--返回后通知-->
            <aop:after-returning method="doAfterReturning" returning="ret" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

运行,因为我们编写的createUser方法没有返回值,所以返回后通知的ret为null

最后就是异常通知了,这个通知的也要传入两个参数,第一个一样,第二个传入Throwable,为方法抛出的异常。

代码语言:javascript复制
    /**
     * 异常通知
     * @param joinPoint 连接点
     * @param th 方法抛出的异常
     */
    public void doAfterThrowing(JoinPoint joinPoint,Throwable th){
        System.out.println("<----异常后通知:" th.getMessage());
    }

配置xml文件

代码语言:javascript复制
    <!--AOP配置-->
    <bean id="methodAspect" class="com.haiexijun.aspect.MethodAspect"/>
    <aop:config>
        <!--pointcut是切点的意思,使用execution表达式描述切面的作用范围-->
        <!--下面的execution表达式说明切面作用于com.haiexijun包下的所有类的所有方法上-->
        <aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*Service.*(..))"/>
        <!--定义切面类-->
        <aop:aspect ref="methodAspect">
            <!--before通知,代表在目标方法运行前先执行切面类里面的方法-->
            <aop:before method="printExecutionTime" pointcut-ref="pointcut"/>
            <!--after通过-->
            <aop:after method="doAfter" pointcut-ref="pointcut"/>
            <!--返回后通知-->
            <aop:after-returning method="doAfterReturning" returning="ret" pointcut-ref="pointcut"/>
            <!--异常通知-->
            <aop:after-throwing method="doAfterThrowing" pointcut-ref="pointcut" throwing="th"/>
        </aop:aspect>
    </aop:config>

我们模拟一个异常

代码语言:javascript复制
    public void createUser(){
        if (1==1){
            throw new RuntimeException("用户已存在");
        }
        System.out.println("执行创建用户的业务逻辑");
        userDao.insert();
    }

下面运行一下代码:

2.环绕通知

本节用一个案例来讲解环绕通知的使用方法。 在我们实际工作中,你可能会遇到这种情况,由于数据的不断累积,用户量的不断增大。可能会导致我们生产环境中系统越来越慢,那我们如何定位到,到是哪个方法执行慢呢?实际开发起来其实不那么轻松,因为一个大型系统类和方法可能有成千上万。难道我们要为每一个方法都去增加相应的代码,捕捉他们的执行时间吗?这样做效率实在是太差了。那说到这里想必你马上就能反映过来,AOP可以非常好的解决这个问题。我们只需要在方法执行前捕捉方法的开始时间,在方法执行后,捕捉方法的结束时间。这两者一相减,不就知道方法执行了多长时间吗?如果这个时间超过了规定范围的话,我们就将其输出保存在日志里面。那这里又延伸出一个新问题。作为AOP中的五种通知类型,到底用前置通知还是后置通知呢?其实都不行,因为我既要在运行前保存一个时间,又要在运行后保存一个时间,这肯定是单个通知无法做到的。不过好在spring为我们提供了一个功能最强大的选择—环绕通知。利用环绕通知,我们可以控制目标方法完整的运行周期,下面我么们通过实例来进行讲解。

首先,我们回到之前的项目中: 里面的两个dao和两个service,我们要在它们每一个方法上进行时间的检查,如果单个方法时间超过一秒中,我们就认为这个方法执行太慢,需要优化。下面我们基于spring AOP来完成。

下面来编写切面类的切面方法: 环绕通知里面用的就不是JoinPoint了,而是用ProceedingJoinPoint,ProceedingJoinPoint是一个特殊的连接点。是JoinPoint的升级版,在原有功能外,还可以控制目标方法是否执行。ProceedingJoinPoint有一个proceed()方法,用于执行目标方法,该proceed()方法会返回一个Object对象。这个Object对象就是目标方法执行后的返回值。

代码语言:javascript复制
package com.haiexijun.aspect;


import org.aspectj.lang.ProceedingJoinPoint;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 切面类
 */
public class MethodAspect {

    /**
     * 环绕通知
     * @param pjp ProceedingJoinPoint是一个特殊的连接点。是JoinPoint的升级版,在原有
     *            功能外,还可以控制目标方法是否执行。
     */
    public Object printExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
        try {
            //得到起始时间
            Long startTime=new Date().getTime();
            Object obj= pjp.proceed();//执行目标方法
            //得到结束时间
            Long endTime=new Date().getTime();
            //执行时间
            long runTime=endTime-startTime;

            //如果目标方法的运行时间超过了1秒,就输出日志
            if (runTime>1000){
                String className=pjp.getTarget().getClass().getName();
                String methodName=pjp.getSignature().getName();
                SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
                String now =sdf.format(new Date());
                System.out.println("======" now ":" className "." methodName " ( " runTime " ms) ====== ");
            }

            return obj;

        } catch (Throwable e) {
            //环绕通知,它通常只会去捕获对应异常的产生
            //但如果目标方法产生了异常,作为产生的异常
            //大多数情况下,它会向外抛出去。
            //把异常抛出去是因为在我们当前系统中,未来运行时可能
            //并不只有一个通知,那如果在当前的环绕通知中,对这个异常进行了消化
            //那就意味着其他后序的处理都不会捕捉到这个异常,就可能会产生一些意料之外的问题
            System.out.println("Exception message:" e.getMessage());
            throw e;
        }

    }

}

之后在xml里面配置环绕通知:

代码语言:javascript复制
    <!--AOP配置-->
    <bean id="methodAspect" class="com.haiexijun.aspect.MethodAspect"/>
    <aop:config>
        <aop:pointcut id="pointcut" expression="execution(public * com.haiexijun..*.*(..))"/>
        <aop:aspect ref="methodAspect">
            <!--环绕通知-->
            <aop:around method="printExecutionTime" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>

然后为了测试,让createUser睡眠几秒钟:

代码语言:javascript复制
    public void createUser(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行创建用户的业务逻辑");
        userDao.insert();
    }

然后,编写程序执行的入口:

代码语言:javascript复制
package com.haiexijun;

import com.haiexijun.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringApplication {
    public static void main(String[] args) {
        ApplicationContext context=new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        UserService userService= context.getBean("userService", UserService.class);
        userService.createUser();
    }
}

运行,会输出超时的方法的日志:

四.基于注解配置Spring AOP

我还是用之前的项目来演示,把applicationContext的bean和AOP都去掉。然后通过注解来配置,然后再xml设置注解扫描。

代码语言:javascript复制
<?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"
       xmlns:context="http://www.springframework.org/schema/context"
       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.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    
    <!--开启组件注解扫描-->
    <context:component-scan base-package="com.haiexijun"/>
    <!--启用Spring IoC的注解模式-->
    <aop:aspectj-autoproxy/>
    
</beans>

为dao和service增加注解: UserDao.java

代码语言:javascript复制
package com.haiexijun.dao;

import org.springframework.stereotype.Repository;

/**
 *用户表Dao
 */
@Repository
public class UserDao {
    public void insert(){
        System.out.println("新增用户数据");
    }
}

EmployeeDao.java

代码语言:javascript复制
package com.haiexijun.dao;

import org.springframework.stereotype.Repository;

/**
 * 员工表Dao
 */
@Repository
public class EmployeeDao {
    public void insert(){
        System.out.println("新增员工数据");
    }
}

EmployeeService.java

代码语言:javascript复制
package com.haiexijun.service;

import com.haiexijun.dao.EmployeeDao;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 员工服务
 */

@Service
public class EmployeeService {

    @Resource
    private EmployeeDao employeeDao;
    public void entry(){
        System.out.println("执行员工入职业务逻辑");
        employeeDao.insert();
    }

    public EmployeeDao getEmployeeDao() {
        return employeeDao;
    }

    public void setEmployeeDao(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }
}

UserService.java

代码语言:javascript复制
package com.haiexijun.service;

import com.haiexijun.dao.UserDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

/**
 * 用户服务
 */

@Service
public class UserService {

    @Resource
    private UserDao userDao;

    public void createUser(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行创建用户的业务逻辑");
        userDao.insert();
    }

    public String generateRandomPassword(String type,Integer length){
        System.out.println("按" type "方式生成" length "位随机密码");
        return "abcdeffdasf";
    }

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

上面都是IoC的配置,下面来进行AOP的配置

打开切面类,进行如下的配置:

代码语言:javascript复制
package com.haiexijun.aspect;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 切面类
 */

@Component//标记当前类为一个IoC的组件
@Aspect //说明当前类是一个切面类
public class MethodAspect {

    /**
     * 环绕通知
     * @param pjp ProceedingJoinPoint是一个特殊的连接点。是JoinPoint的升级版,在原有
     *            功能外,还可以控制目标方法是否执行。
     */
    //环绕通知,参数为PointCut切点表达式
    @Around("execution(public * com.haiexijun..*.*(..))")
    public Object printExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
        try {
            //得到起始时间
            Long startTime=new Date().getTime();
            Object obj= pjp.proceed();//执行目标方法
            //得到结束时间
            Long endTime=new Date().getTime();
            //执行时间
            long runTime=endTime-startTime;

            //如果目标方法的运行时间超过了1秒,就输出日志
            if (runTime>1000){
                String className=pjp.getTarget().getClass().getName();
                String methodName=pjp.getSignature().getName();
                SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
                String now =sdf.format(new Date());
                System.out.println("======" now ":" className "." methodName " ( " runTime " ms) ====== ");
            }

            return obj;

        } catch (Throwable e) {
            //环绕通知,它通常只会去捕获对应异常的产生
            //但如果目标方法产生了异常,作为产生的异常
            //大多数情况下,它会向外抛出去。
            //把异常抛出去是因为在我们当前系统中,未来运行时可能
            //并不只有一个通知,那如果在当前的环绕通知中,对这个异常进行了消化
            //那就意味着其他后序的处理都不会捕捉到这个异常,就可能会产生一些意料之外的问题
            System.out.println("Exception message:" e.getMessage());
            throw e;
        }

    }



}

当然,还有其他的通知的注解,比如@After、@Before、@AfterReturning等。这里不做演示了。

最后,我们打开程序入口类,运行一下,没有问题:

五.AOP中的代理模式的应用

1.Spring AOP的实现原理

在未来,我们去找工作的时候,有一个问题,面试官会经常提起。那就是请你给我讲一下Spring AOP底层的实现原理是什么? 这个问题实在是太常见了,那作为Spring AOP底层的实现原理是什么呢?如下:

Spring是基于代理模式实现功能动态拓展,包含以下两种形式: 第一种是如果目标类实现了接口,则会通过JDK动态代理实现功能拓展。第二种是如果目标类没有实现接口,则Spring AOP底层通过CGLib这个第三方的组件实现功能的扩展。

此时就涉及到一个核心的问题,什么是代理模式?

2.静态代理

代理模式是指通过代理对象对原对象实现功能拓展。

那代理对象又是什么呢?其实在我们日常生活中随处可见,比如你去到一个全新的城市,希望租一套房子。这时你会怎么做,难道是我到处从电线杆上看这些求租的信息吗?肯定不会,大多数人的第一选择是找到中介公司,通过中介系统的数据库,去查询附近有哪些符合我要求的房子,看价钱是否合适。如果觉得还OK,那就由中介带着我去实地考察,看一下我是否满意,如果满意,那就成交,不满意的话,就继续找。而反过来,房东也依赖于中介。因为房东往往也有自己的工作,不可能天天拿着钥匙给租房的人开门去吧?这时房东就可以委托中介,房东把钥匙给中介,让中介的人带租房的人去看房子。可以看到中介的办事人员就是一个典型的代理人。这个案例放到我们程序中就称之为代理模式。

所谓代理模式,其核心的理念是我们要去创建一个代理类,在代理类中,持有最原始的委托类。作为代理类和委托类,他们要共同实现相同的接口。而客户则是则是通过代理类,完成客户所需要的功能。按照刚才的例子,那个客户类呢,就是租房的人。代理类就是中介的办事人员,而委托类是房东。作为中介和房东,他们的目的是一致的。都是要把房子给租出去。正是因为有着相同的目的,所以他们就实现了共同的接口,这个接口中提供了一个租房的方法,代理类和委托类都去实现租房的这个逻辑。作为代理类,它内部持有了委托类的对象,所以在代理类被实例化以后,也就是代理对象执行的过程中,可以对原始的逻辑产生额外的行为。比如说这个中介代理类,在为客户看完房子以后,除了交付给原始的房东原始的租金以外,它还要向客户收取代理费,这就是额外的扩展逻辑了。放在程序中也是一样的。

那作为代理模式,我们如何去实现呢?下面通过代码来演示一下。

下面来创建一个新的Maven工程: 增加一个service包,在service包中增加一个接口。刚才强调过,无论是代理类和委托类他们都要实现相同的接口,这个接口名为UserService。我们模拟一下现实的环境,在这个用户服务接口中,提供一个createUser()方法,所有实现类都要去实现这个方法。那与此同时,在service包下再创建一个新的实现类,名为UserServiceImpl,它实现UserService接口和里面的方法。我们示意性书写一下。

UserService.java

代码语言:javascript复制
package com.haiexijun.service;

public interface UserService {
    public void createUser();
}

UserServiceImpl.java

代码语言:javascript复制
package com.haiexijun.service;

public class UserServiceImpl implements UserService {

    @Override
    public void createUser() {
        System.out.println("执行创建用户的业务逻辑");
    }
}

作为这段代码,我们调用其实非常简单,新增加一个Application类,在类中增加Main方法,然后编写代码:

代码语言:javascript复制
package com.haiexijun.service;

public class Application {
    public static void main(String[] args) {
        UserService userService=new UserServiceImpl();
        userService.createUser();
    }
}

然后运行,当然没问题。

但是我提出新的要求,希望将这个方法执行的时间打印出来,该这么办呢?这个需求我们之前在学习Spring AOP的时候已经遇到过了。但是是通过开发切面类来完成的。但是,如果放在我们代理模式中如何做呢?如果需要实现这个功能的扩展,就必须基于UserService接口创建对应的代理类。同时在代理类中去持有与之对应的具体实现。

下面来看一下具体做法: 在service包下面创建一个全新的类,名为UserServiceProxy 。代理的英文单词就是proxy。作为当前的代理类,其核心特点就是持有委托类的对象。定义一个私有的UserService类型的属性。接下来,关键的地方来了,这里定义一个带参的构造方法,参数为UserService。这个参数是在我们代理类实例化的时候,从外侧传入进来的,同时对内部的UserService来进行赋值。这样是不是就相当于在我们创建代理对象的时候,通过外侧传入的某个UserService的实现类,为内部的这个类的UserService赋值,相当于持有委托类的对象了。于此同时,不要忘记作为代理类和委托类都要实现相同的接口,也就是UserService,然后实现createUser()方法。在当前的代理类方法中,因为之前已经持有了委托类的对象,我们可以在createUser方法中发起委托类具体的职责,比如所createUser。同时,在这个方法执行前,我们还可以扩展其他的代码,比如对当前时间运行时的时间。这是不是就是功能的拓展啊?

UserServiceProxy.java

代码语言:javascript复制
package com.haiexijun.service;

import java.text.SimpleDateFormat;
import java.util.Date;

public class UserServiceProxy implements UserService {
    //持有委托类的对象
    private UserService userService;
    public UserServiceProxy(UserService userService){
        this.userService=userService;
    }

    @Override
    public void createUser() {
        System.out.println("======" new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()));
        userService.createUser();
    }
}

与此同时,作为我们的客户,也就是使用者来说,不在直接面向UserServiceImpl,取而代之的是UserServiceProxy ,去面向这个代理类来调用。那么在代理类调用时,需要传入一个具体的UserServiceImpl。

application.java

代码语言:javascript复制
package com.haiexijun.service;

public class Application {
    public static void main(String[] args) {
        UserService userService=new UserServiceProxy(new UserServiceImpl());
        userService.createUser();
    }
}

运行结果,这就是通过代理类实现功能的额外扩展

代理模式可以嵌套使用的。 再打个比方,你手不是见过这样的情况,某一个租户,他把房子整栋租下来,然后再把房间租给很多其他的租客,这种形式在北京上海深圳这种房价高的地方,是很常见的。我们也称这种租客叫二房东。

那在代理模式中也是支持的。因为委托类和代理类都实现了相同的接口。同时,在创建对象的时候,又允许传入对应接口的实现类。因此,我们可以再创建一个全新的代理UserServiceProxy1,具体的做法和之前是一样的,实现UserService接口和方法。然后我们可以为create内扩展一些其他的业务代码,在UserService的createUser方法后拓展执行一个输出语句。这时,系统中就出现了两个代理类,一个是在createUser方法前执行,一个是在createUser方法后执行拓展。

代码语言:javascript复制
package com.haiexijun.service;

public class UserServiceProxy1  implements UserService{

    private UserService userService;
    public UserServiceProxy1(UserService userService){
        this.userService=userService;
    }

    @Override
    public void createUser() {
        userService.createUser();
        System.out.println("======后置扩展功能=====");
    }
}

那租客肯定是要面向二房东来执行的,下面就是new UserServiceProxy1了,里面传一房东UserServiceProxy。感觉有一点套娃。

代码语言:javascript复制
package com.haiexijun.service;

public class Application {
    public static void main(String[] args) {
        UserService userService=new UserServiceProxy1(new UserServiceProxy(new UserServiceImpl()));
        userService.createUser();
    }
}

运行后,发现前置和后置的额外拓展功能都打印出来了。

这就是代理模式的精妙之处。可以实现对功能的无限层次的拓展。但在这里,我们每进行一次功能的拓展,都要自己来创建一个代理类啊,这样有一个缺点,随着我们的功能的不断地扩大,每一个具体的实现类,他都要至少拥有一个代理类。而这个代理类是要按照这个规则来自己进行书写的。这样呢,假如我们系统中,有成百上千个具体的业务实现类,那就意味着,也有成百上千个具体的代理类来为具体实现类实现扩展职责。这会让我们系统变得无比的臃肿。对于这种必须要手动创建代理类的使用方式,我们称之为静态代理。 静态代理是最简单的一种代理模式的使用方式,但是也是最麻烦的一种使用方式。

那说起手动创建,就有与之对应的自动创建。在JDK1.2以后,由于反射机制的引入,为我们自动创建代理类提供了可能。那下面就来学习与静态代理对应的动态代理。

3.AOP底层原理—JDK动态代理

下面新创建一个Maven工程,因为JDK动态代理的功能不用到其他第三方组件,所以我们不用在Maven中添加任何依赖。然后,把上一个案例的UserService接口和UserServiceImpl实现类拿过来。

要想实现基于JDK动态代理来实现UserServiceImpl的功能扩展,首先要在service包下再额外的创建一个类ProxyInvocationHandler ,这个类要实现一个至关重要的接口InvocationHandler 。我们要实现它的invoke方法。这个invoke是不是在那里遇见过呢?没错,是在之前学习反射中遇到过,通过invoke来调用目标方法。这里的invoke也是一样的道理。

ProxyInvocationHandler实现InvocationHandler,它的职能非常明确。InvocationHandler是JDK提供的反射类,用于在JDK动态代理中对目标方法进行增强。 InvocationHandler实现类与Spring AOP的切面类的环绕通知类似。我们在invoke方法里面对目标方法进行增强。其中,invoke方法包含了3个参数。第一个参数Object代表了代理类对象,作为这个代理类对象,通常是由我们JDK动态代理自动生成的。第二个参数Method是目标方法对象,说明了目标方法的信息,包括方法名等。而第三个参数是一个Object数组,表示目标方法的实参。该方法返回一个Object,代表目标方法运行后的返回值。而最后抛出Throwable表示抛出目标方法异常。

代码语言:javascript复制
package haiexijun.service;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ProxyInvocationHandler implements InvocationHandler {

    private Object target;//目标对象

    public ProxyInvocationHandler(Object target) {
        this.target = target;
    }

    /**
     * 在invoke方法对目标方法进行增强
     * @param proxy 代理类对象
     * @param method 目标方法
     * @param args 目标方法的实参
     * @return 目标方法运行后的返回值
     * @throws Throwable 目标方法抛出的异常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("======" new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()) " ========");
        Object ret =method.invoke(target,args);//调用目标方法
        return ret;
    }
}

然后在Application类中编写代码使用:

代码语言:javascript复制
package haiexijun.service;

import java.lang.reflect.Proxy;

public class Application {
    public static void main(String[] args) {
        //UserService为目标对象
        UserService userService=new UserServiceImpl();
        ProxyInvocationHandler invocationHandler= new ProxyInvocationHandler(userService);
        //invoke方法要从传入代理类Proxy
        //创建动态代理类,
        // 通过newProxyInstance方法来创建,传入类加载器,类要实现的接口,还有对目标方法进行扩展的InvocationHandler
        UserService userServiceProxy  = (UserService)Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),invocationHandler);

        userServiceProxy.createUser();

    }
}

但是,动态代理必须要实现一个实现类的接口才能够运行,如果没有实现接口,反射过程必然会报错。可是在我们实际情况下,有着大量的类都没有实现接口,该怎么做呢?这时候,Spring又为我们提供了另外一种解决方案。依赖于spring的第三方组件CGLib实现对类的增强。

4.CGLib实现代理类

CGLib是运行时字节码增强技术。全名为Code Generation Library 当我们某一个类它没有实现接口的时候, Spring AOP 会在运行时生成目标继承类字节码的方式进行扩展。

下面来具体逻辑一下所谓生成目标继承类字节码:

上面有一个Service类,里面有一个findById的按id号查询的方法。里面写入具体的业务代码。可以看到里面的Service是没有实现任何接口的,那显然Jdk动态代理无法对其进行扩展。Spring看到这个类没有实现接口,则自动会使用CGLib通过继承的方式来对类进行扩展。这个继承类是在JVM运行过程中自动生成的,他的生成规则是,前面是类的原始名字,后面增加两个$$符号,然后加EnhancerByCGLIB(spring5以后是EnhancerBySpringCGLIB)。然后继承自Service父类。可以对findById方法进行重写,方法里面通过super指向父类的业务代码,并可以添加拓展的前置代码和后置代码等。客户端在调用的时候,面向的是这个增强的子类。 总结: 要增强的目标类实现了接口时,AOP底层调用的是JDK动态代理,没有实现接口时,AOP底层调用的是CGLib代理

0 人点赞