【Spring从入门到实战教程】第三章 Spring AOP详解

三、Spring AOP

3.1 AOP前奏

3.1.1 需求

  • 日志,在程序执行期间追踪正在发生的活动;

  • 验证,程序执行期间处理合法数据;

3.1.2 代码

UserService接口以及实现类:

public interface UserService {
    
    void insertUser();

    void updateUser();

    void deleteUser(int id);

    List<String> selectUser();
}

public class UserServiceImpl implements UserService {
    @Override
    public void insertUser() {
        //在方法种主要业务逻辑代码开始之前,加入日志记录
        System.out.println("日志:用户新增...");

        System.out.println("用户新增的业务逻辑...");
    }

    @Override
    public void updateUser() {
        System.out.println("日志:用户修改...");

        System.out.println("用户修改的业务逻辑...");
    }

    @Override
    public void deleteUser(int id) {
        System.out.println("日志:用户删除...");

        System.out.println("用户删除的业务逻辑...");
    }

    @Override
    public List<String> selectUser() {
        System.out.println("日志:用户查询...");

        System.out.println("用户查询的业务逻辑...");
        return null;
    }
}

测试:

public class ProxyTest {
    @Test
    public void test() {
        UserService userService = new UserServiceImpl();

        userService.insertUser();
        System.out.println("----------------------------------");

        userService.updateUser();
        System.out.println("----------------------------------");

        userService.deleteUser(10);
        System.out.println("----------------------------------");

        List<String> list = userService.selectUser();
        System.out.println(list);
    }
}

3.1.3 问题

  • 代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点;

  • 代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块;

3.2 代理模式

使用代理模式解决上述问题:

     代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象,任何对原始对象的调用都要通过代理,代理对象决定是否以及何时将方法调用转到原始对象上。
    
    通常,代理模式用于处理两种问题:
        1、控制对基础对象的访问;
        2、在访问基础对象时增加额外功能;

3.2.1 静态代理

    每个业务接口都需要有一个对应的代理类,并实现业务接口;

/**
 * 静态代理
 * 要求:代理类与目标类必须实现同一个接口
 */
public class UserServiceProxy implements UserService {
    private UserService userService;

    /**
     * 目的:将代理类和目标类绑定
     * 在创建代理类的同时传入目标类对象
     */
    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void insertUser() {
        //1、代理功能
        System.out.println("日志记录:用户新增...");
        //2、调用目标类的方法,让核心业务逻辑执行
        userService.insertUser();
    }

    @Override
    public void updateUser() {
        System.out.println("日志记录:用户修改...");
        userService.updateUser();
    }

    @Override
    public void deleteUser(int id) {
        System.out.println("日志记录:用户删除..." + id);
        userService.deleteUser(id);
    }

    @Override
    public List<String> selectUser() {
        System.out.println("日志记录:用户查询...");
        return userService.selectUser();
    }
}

移除业务层的日志代码并测试:

@Test
public void testStaticProxy() {
    UserService userService = new UserServiceProxy(new UserServiceImpl());

    userService.insertUser();
    System.out.println("----------------------------------");

    userService.updateUser();
    System.out.println("----------------------------------");

    userService.deleteUser(10);
    System.out.println("----------------------------------");

    List<String> list = userService.selectUser();
    System.out.println(list);
}
  • 优点:简单、易于理解;

  • 缺点:项目庞大后业务接口过多,造成代理类过多;

3.2.2 JDK动态代理

    JDK 动态代理(Dynamic Proxy API),它是 JDK1.3 中引入的特性,核心 API 是 Proxy 类和 InvocationHandler 接口。它的原理是利用反射机制在运行时生成代理类的字节码。

  • JDK动态代理:采用Java反射机制中的动态代理,实现InvocationHandler接口;

  • 要求:目标类必须采用接口 + 实现类的形式;

/**
 * JDK动态代理
 * 必须实现InvocationHandler接口
 */
public class LoggerProxy implements InvocationHandler {
    /**
     * 执行目标(最终要执行业务逻辑类)方法
     *
     * @param proxy  代理类对象
     * @param method 目标方法对象
     * @param args   目标方法的参数值列表
     * @return 目标方法的返回值
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //日志记录的代码
        System.out.println("代理日志:目标方法--前置" + method.getName() + "....");
        
        //执行目标方法(业务逻辑方法)
        Object result_val = null;
        try {
            result_val = method.invoke(targetClass.newInstance(), args);
            System.out.println("代理日志:目标方法--返回" + method.getName() + "....");
        } catch (Exception e) {
            System.out.println("代理日志:目标方法--异常" + method.getName() + "....");
        }
        
        System.out.println("代理日志:目标方法--后置" + method.getName() + "....");
        return result_val;
    }

    /**
     * 目标对象的Class类型
     */
    private Class targetClass;

    /**
     * 获取代理对象:就是动态实现目标类接口的实现类
     * newProxyInstance(): 获取实现目标接口的代理对象
     * 注意:JDK中的动态代理,要求业务类必须是接口+实现类的形式
     */
    public Object getProxy(Class targetClass) {
        this.targetClass = targetClass;
        return Proxy.newProxyInstance(targetClass.getClassLoader(), targetClass.getInterfaces(), this);
    }
}

PersonService接口以及实现类:

public interface PersonService {
    void insertPerson();
}

public class PersonServiceImpl implements PersonService {
    @Override
    public void insertPerson() {
        System.out.println("新增Person的业务逻辑...");
    }
}

测试:

public class ProxyTest {
    @Test
    public void testDynamicProxy() {
        //获取代理对象
        UserService userService = (UserService) new LoggerProxy().getProxy(UserServiceImpl.class);
        System.out.println(userService);

        //通过代理对象调用指定的方法:具体执行流程,代理类调用指定的方法,然后通过invoke调用目标类的方法
        userService.insertUser();
        System.out.println("----------------------------------");

        userService.updateUser();
        System.out.println("----------------------------------");

        userService.deleteUser(10);
        System.out.println("----------------------------------");

        List<String> list = userService.selectUser();
        System.out.println(list);
        System.out.println("----------------------------------");

        PersonService personService = (PersonService) new LoggerProxy().getProxy(PersonServiceImpl.class);
        personService.insertPerson();
    }
}
  • 优点: 一个类可以代理所有的业务;

  • 缺点: 利用反射机制实现,不好理解,不易于维护;JDK动态代理:业务类必须是接口+实现类的形式;

3.2.3 Cglib动态代理

    Cglib动态代理,实现cglib包下的MethodInterceptor接口,目前Spring就是使用的Cglib动态代理。

public class CglibProxy implements MethodInterceptor {
    /**
     * @param obj    CGLib动态生成的代理类对象
     * @param method 目标方法对象
     * @param args   目标方法的参数值列表
     * @param proxy  代理类对方法的代理对象
     * @return 目标方法的返回值
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Cglib动态代理...");
        
        //执行目标方法(业务逻辑方法)
        Object result_val = null;
        try {
            result_val = method.invoke(targetClass.newInstance(), args);
        } catch (Exception e) {
        }
        return result_val;
    }

    /**
     * 目标对象的Class类型
     */
    private Class targetClass;

    /**
     * 获取代理对象
     * <p>
     * 注意:JDK中的动态代理,要求业务类必须是接口+实现类的形式
     */
    public Object getProxy(Class targetClass) {
        //为目标对象target赋值
        this.targetClass = targetClass;
        Enhancer enhancer = new Enhancer();
        //设置父类,因为Cglib是针对指定的类生成一个子类,所以需要指定父类
        enhancer.setSuperclass(targetClass);
        //设置回调
        enhancer.setCallback(this);
        //创建并返回代理对象
        Object result = enhancer.create();
        return result;
    }
}

测试:

public class ProxyTest {
   @Test
    public void testCglibProxy() {
        UserService userService = (UserService) new CglibProxy().getProxy(UserServiceImpl.class);
        System.out.println(userService);

        userService.insertUser();
        System.out.println("----------------------------------");

        userService.updateUser();
        System.out.println("----------------------------------");

        userService.deleteUser(10);
        System.out.println("----------------------------------");

        List<String> list = userService.selectUser();
        System.out.println(list);
    }
}

3.3 AOP概述

    AOP 的全称是“Aspect Oriented Programming”,译为“面向切面编程”,和 OOP(面向对象编程)类似,它也是一种编程思想。
    
    通常情况下,我们会根据业务使用 OOP(面向对象)思想,将应用划分为多个不同的业务模块,每个模块的核心功能都只为特定的业务领域提供服务,例如电商系统中的订单模块、商品模块、库存模块就分别是为维护电商系统的订单信息、商品信息以及库存信息而服务的。

    但除此之外,应用中往往还存在一些非业务的通用功能,例如日志管理、权限管理、事务管理、异常管理等。这些通用功能虽然与应用的业务无关,但几乎所有的业务模块都会使用到它们,因此这些通用功能代码就只能横向散布式地嵌入到多个不同的业务模块之中。这无疑会产生大量重复性代码,不利于各个模块的复用。

    大家可能会想,可以将这些重复性代码封装成为公共函数,然后在业务模块中显式的调用,不也能减少重复性代码吗?是的,这样做的确能一定程度上减少重复性代码,但这样也增加了业务代码与公共函数的耦合性,任何对于公共函数的修改都会对所有与之相关的业务代码造成影响。

    与 OOP 中纵向的父子继承关系不同,AOP 是通过横向的抽取机制实现的。它将应用中的一些非业务的通用功能抽取出来单独维护,并通过声明的方式(例如配置文件、注解等)定义这些功能要以何种方式作用在那个应用中,而不是在业务模块的代码中直接调用。

    这虽然设计公共函数有几分类似,但传统的公共函数除了在代码直接硬调用之外并没有其他手段。AOP 则为这一问题提供了一套灵活多样的实现方法(例如 Proxy 代理、拦截器、字节码翻译技术等),可以在无须修改任何业务代码的基础上完成对这些通用功能的调用和修改。

    AOP 编程和 OOP 编程的目标是一致的,都是为了减少程序中的重复性代码,让开发人员有更多的精力专注于业务逻辑的开发,只不过两者的实现方式大不相同。

    OOP 就像是一根“绣花针”,是一种婉约派的选择,它使用继承和组合方式,仔细地为所有涉及通用功能的模块编制成一套类和对象的体系,以达到减少重复性代码的目标。而 AOP 则更像是一把“砍柴刀”,是一种豪放派的选择,大刀阔斧的规定,凡是某包某类下的某方法都一并进行处理。

    AOP 不是用来替换 OOP 的,而是 OOP 的一种延伸,用来解决 OOP 编程中遇到的问题。

  • AOP的主要编程对象是切面(aspect),而切面就是模块化的横切关注点;

    • 关注点:增强代码所放置的地方。比如在学生信息管理的Service层添加日志记录,那么Service层就是关注点;

    • 横切:常规模块的业务流程是Web -> Service -> Dao,如果我们想要在所有模块的Service层都进行日志记录,那么这就是横切;

    • 模块化:把以前分散在各个模块中的增强代码提取出来,集中在某个模块或类中,那么这就是模块化;

  • 在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的对象(切面)里;

  • AOP的好处:每个事物逻辑位于一个位置,代码不分散,便于维护和升级。业务模块更简洁,只包含核心业务代码;

3.4 AOP术语

名称 说明
Joinpoint(连接点) AOP 的核心概念,指的是程序执行期间明确定义的一个点,例如方法的调用、类初始化、对象实例化等。 在 Spring 中,连接点则指可以被动态代理拦截目标类的方法。
Pointcut(切入点) 又称切点,指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。
Advice(通知) 指拦截到 Joinpoint 之后要执行的代码,即对切入点增强的内容。
Target(目标) 指代理的目标对象,通常也被称为被通知(advised)对象。
Weaving(织入) 指把增强代码应用到目标对象上,生成代理对象的过程。
Proxy(代理) 指生成的代理对象。
Aspect(切面) 通知和切入点共同组成了切面。

示例:

通知类型:

通知 说明
before(前置通知) 通知方法在目标方法调用之前执行
after(后置通知) 通知方法在目标方法返回或异常后调用
after-returning(返回后通知) 通知方法会在目标方法返回后调用
after-throwing(抛出异常通知) 通知方法会在目标方法抛出异常后调用
around(环绕通知) 通知方法会将目标方法封装起来

3.5 AOP的开发步骤

3.5.1 加入相应的jar包

方式一:引入jar包

 方式二:maven依赖配置

<dependencies>
    <!-- spring核心包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>4.3.18.RELEASE</version>
    </dependency>
    <!-- springbean包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>4.3.18.RELEASE</version>
    </dependency>
    <!-- springcontext包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.3.18.RELEASE</version>
    </dependency>
    <!-- spring表达式包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-expression</artifactId>
        <version>4.3.18.RELEASE</version>
    </dependency>
    <!-- springAOP包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>4.3.18.RELEASE</version>
    </dependency>
    <!-- springAspects包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>4.3.18.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

3.5.2 命名空间以及标签规范

在spring的配置文件中加入aop命名空间以及aop标签规范:

xmlns:aop="http://www.springframework.org/schema/aop"

http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

3.5.3 编写切面类

定义一个普通Java类,在其中添加功能方法:

/**
 * 日志切面类:日志记录功能
 */
public class LogAspect {

    /**
     * 前置通知:在目标方法执行之前实现的功能
     */
    public void beforeMethod() {
        System.out.println("AOP日志记录:前置通知......");
    }
}

切面完整代码:

/**
 * 日志切面类:日志记录功能
 */
public class LogAspect {

    /**
     * 前置通知:在目标方法执行之前实现的功能
     * 参数:JoinPoint连接点对象,可获取当前要执行的目标方法的信息
     */
    public void beforeMethod(JoinPoint joinPoint){
        //通过连接点对象获取方法的名称
        String methodName = joinPoint.getSignature().getName();
        //方法的参数值列表
        Object[] args = joinPoint.getArgs();
        System.out.println("AOP日志记录:前置通知......" + methodName + Arrays.toString(args));
    }
    /**
     * 后置通知:在目标方法执行之后实现的功能
     */
    public void afterMethod(JoinPoint joinPoint){
        //通过连接点对象获取方法的名称
        String methodName = joinPoint.getSignature().getName();
        //方法的参数值列表
        Object[] args = joinPoint.getArgs();
        System.out.println("AOP日志记录:后置通知......" + methodName + Arrays.toString(args));
    }

    /**
     * 返回通知:在目标方法有返回值之后实现的功能
     * 通过方法的参数获取目标方法的返回值数据
     */
    public void afterReturnMethod(JoinPoint joinPoint, Object resultValue){
        //通过连接点对象获取方法的名称
        String methodName = joinPoint.getSignature().getName();
        //方法的参数值列表
        Object[] args = joinPoint.getArgs();
        System.out.println("AOP日志记录:返回通知......" + methodName + Arrays.toString(args));
        System.out.println("方法的返回值为:" + resultValue);
    }

    /**
     * 异常通知:在目标方法抛出异常之后实现的功能
     * 通过方法参数获取目标方法抛出的异常对象
     */
    public void afterThrowMethod(JoinPoint joinPoint, Exception ex){
        //通过连接点对象获取方法的名称
        String methodName = joinPoint.getSignature().getName();
        //方法的参数值列表
        Object[] args = joinPoint.getArgs();
        System.out.println("AOP日志记录:异常通知......" + methodName + Arrays.toString(args));
        System.out.println("方法抛出的异常对象:" + ex);
    }

    /**
     * 环绕通知:综合以上所有的通知
     * 环绕通知必须配置ProceedingJoinPoint参数
     */
    public void aroundMethod(ProceedingJoinPoint proceedingJoinPoint){
        System.out.println("AOP日志记录:环绕通知之前置......");
        try {
            //手动执行目标方法
            //proceed()就是在执行目标方法,其返回值为目标方法的返回值
            Object resultValue = proceedingJoinPoint.proceed();
            System.out.println("AOP日志记录:环绕通知之返回......" + resultValue);
        } catch (Throwable throwable) {
            System.out.println("AOP日志记录:环绕通知之异常......" + throwable);
            throwable.printStackTrace();
        }
        System.out.println("AOP日志记录:环绕通知之后置......");
    }
}

3.5.4 配置AOP

  • 将目标类和切面类都配置到IOC容器中;

  • 配置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:util="http://www.springframework.org/schema/util"
       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/util
		http://www.springframework.org/schema/util/spring-util.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">

    <!--1.配置bean-->
    <bean id="userService" class="com.newcapec.service.impl.UserServiceImpl"/>
    <bean id="personService" class="com.newcapec.service.impl.PersonServiceImpl"/>
    <bean id="log" class="com.newcapec.aspect.LogAspect"/>

    <!--2.配置AOP-->
    <aop:config>
        <!--
            公用切点表达式
                id: 切点表达式的名称
                expression: 切点表达式的内容
            注:
                aop:pointcut定义在aop:config中,是公用级别的切点表达式
                aop:pointcut定义在aop:aspect中,是切面级别的切点表达式
     -->
        <aop:pointcut id="exp1" expression="execution(* com.newcapec.service.impl.*Impl.*(..))"/>
        <aop:pointcut id="exp2" expression="execution(public void com.newcapec.service.impl.UserServiceImpl.insertUser())"/>
        <aop:pointcut id="exp3" expression="execution(* com.newcapec.service.impl.UserServiceImpl.selectUser(..))"/>
        <aop:pointcut id="exp4" expression="execution(* com.newcapec.service.impl.PersonServiceImpl.insertPerson(..))"/>

        <!--2.1 配置切面类-->
        <aop:aspect ref="log">
            <!--2.2 配置通知-->
            <!--
                通知类型:
                    <aop:before>:表示前置通知
                    <aop:after>:表示后置通知
                    <aop:after-returning>:表示返回通知
                    <aop:after-throwing>:表示异常通知
                    <aop:around>:表示环绕通知
                通知类型属性:
                    method: 切面对象中执行该通知的方法,通知的方法名称
                    pointcut: 切点表达式
                        告知spring,通知应用在哪些方法(目标方法,业务逻辑方法)上
                    pointcut-ref: 引用公共切点表达式
                    returning: 在返回通知中接收方法返回值的参数名称
                    throwing: 在异常通知中接收方法抛出的异常对象的参数名称
            -->
            <!--2.3 配置切点表达式-->
            <!--
                编写规则:以execution()包裹起来的内容
                通配符:
                    1. 星号 *
                    2. 双句号 ..
                        使用在包中表示该包下以及子包下
                        使用在参数类型列表中表示不限制方法的参数类型
                示例:
                    1.单个方法,具体方法
                    语法:execution(方法访问修饰符 返回值类型 方法所在的包名.类名.方法名(参数类型,...))
                    例如:execution(public void com.newcapec.dao.UserDaoImpl.insertUser())
                    2.多个方法,批量方法:*符号作为通配符
                    语法:execution(* 方法所在的包名.类名.*(..))
                    例如:execution(* com.newcapec.dao.UserDaoImpl.*(..))
                    例如:execution(* com.newcapec.*.dao.*.*(..))
                    3.通配,所有方法
                    语法:execution(* *.*.*(..))
            -->

            <!--精准匹配-->
            <!--<aop:before method="beforeMethod" pointcut="execution(public void com.newcapec.service.impl.UserServiceImpl.insertUser())" />-->
            <!--模糊匹配-->
            <!--<aop:before method="beforeMethod" pointcut="execution(* com.newcapec.service..*Impl.*(..))"/>-->

            <aop:before method="beforeMethod" pointcut-ref="exp1"/>
            <aop:after method="afterMethod" pointcut-ref="exp2"/>
            <aop:after-returning method="afterReturnMethod" pointcut-ref="exp3" returning="resultValue"/>
            <aop:after-throwing method="afterThrowMethod" pointcut-ref="exp4" throwing="ex"/>
            <!--<aop:around method="aroundMethod" pointcut-ref="exp1"/>-->
        </aop:aspect>
    </aop:config>
</beans>

3.5.5 测试

public class AOPTest {

    @Test
    public void test() {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

        UserService userService = ac.getBean("userService", UserService.class);

        userService.insertUser();

        System.out.println("----------------------------------");

        userService.updateUser();
        System.out.println("----------------------------------");

        userService.deleteUser(1);
        System.out.println("----------------------------------");

        userService.selectUser();
        System.out.println("----------------------------------");

        PersonService personService = ac.getBean("personService", PersonService.class);
        personService.insertPerson();
    }
}

注意:在目标方法出现异常后,返回通知不再执行。但是在目标方法没有出现异常时,异常通知不会执行。

3.5.6 切面的优先级

<aop:aspect>标签中配置order属性,属性值为数字。数字越小优先级越高,该切面功能越先被执行

/**
 * 另一个切面类
 */
public class OtherAspect {

    public void beforeM(){
        System.out.println("OtherAspect的beforeM方法.....");
    }
}
<bean id="other" class="com.newcapec.aspect.OtherAspect"/>

<aop:config>
    <!--
        切面优先级:决定哪个切面中通知先执行,哪个后执行
        在前置通知中:优先级高的先执行,优先级低的后执行
        在后置通知中:优先级高的后执行,优先级低的先执行
        <aop:aspect>标签中的order属性决定了优先级的高低,其值越小优先级越高
    -->
    <aop:aspect ref="log" order="2">
    </aop:aspect>
    <aop:aspect ref="other" order="1">
        <aop:before method="beforeM" pointcut-ref="exp1"/>
    </aop:aspect>
</aop:config>

猜你喜欢

转载自blog.csdn.net/ligonglanyuan/article/details/124787652