Spring之AOP(一)

135 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情


1 AOP简介

  • AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming, 面向对象编程) 的补充。
  • AOP 的主要编程对象是切面(aspect), 而切面模块化横切关注点。
  • 在应用 AOP 编程时, 仍然需要定义公共功能, 但可以明确的定义这个功能在哪里, 以什么方式应用, 并且不必修改受影响的类. 这样一来横切关注点就被模块化到特殊的对象(切面)里。
  • AOP 的好处:每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级业务模块更简洁, 只包含核心业务代码。

2 动态代理

示例:实现一个简单的加减乘除计算,添加日志,在程序执行期间追踪正在发生的活动。

接口类IArithmeticCalculator:

package site.exciter.spring.aop.helloworld;

public interface IArithmeticCalculator {
    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}

普通方式 在实现类中的各个方法中添加日志:

package site.exciter.spring.aop.helloworld;

/**
 * 普通方式添加log
 * 缺点:
 * 1、每个方法中非核心业务代码越来越多不好维护
 * 2、冗余代码多
 * 3、如果需求变更,需要修改多处代码
 */
public class ArithmeticCalculatorLoggingImpl implements IArithmeticCalculator {

    @Override
    public int add(int i, int j) {
        System.out.println("The method add begin with [" + i + "+" + j + "]");
        int result = i + j;
        System.out.println("The method add end with [" + result + "]");
        return result;
    }

    @Override
    public int sub(int i, int j) {
        System.out.println("The method sub begin with [" + i + "-" + j + "]");
        int result = i - j;
        System.out.println("The method sub end with [" + result + "]");
        return result;
    }

    @Override
    public int mul(int i, int j) {
        System.out.println("The method mul begin with [" + i + "*" + j + "]");
        int result = i * j;
        System.out.println("The method mul end with [" + result + "]");
        return result;
    }

    @Override
    public int div(int i, int j) {
        System.out.println("The method div begin with [" + i + "/" + j + "]");
        int result = i / j;
        System.out.println("The method div end with [" + result + "]");
        return result;
    }
}

测试:

@org.junit.Test
    public void testLoggingImpl() {
        ArithmeticCalculatorLoggingImpl calculator = new ArithmeticCalculatorLoggingImpl();

        int resultAdd = calculator.add(1, 3);
        System.out.println("--->" + resultAdd);

        int resultMul = calculator.mul(2, 3);
        System.out.println("--->" + resultMul);
    }

出现的问题:

  • 代码混乱:越来越多的非业务需求(日志和验证等)加入后, 原有的业务方法急剧膨胀. 每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
  • 代码分散: 以日志需求为例, 只是为了满足这个单一需求, 就不得不在多个模块(方法)里多次重复相同的日志代码. 如果日志需求发生变化, 必须修改所有模块。

使用动态代理解决:

新建一个纯净的计算方法实现类ArithmeticCalculatorLoggingImplSimple:

package site.exciter.spring.aop.helloworld;

public class ArithmeticCalculatorLoggingImplSimple implements IArithmeticCalculator {

    @Override
    public int add(int i, int j) {
        return i + j;
    }

    @Override
    public int sub(int i, int j) {
        return i - j;
    }

    @Override
    public int mul(int i, int j) {
        return i * j;
    }

    @Override
    public int div(int i, int j) {
        return i / j;
    }
}

使用代理对象:

package site.exciter.spring.aop.helloworld;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * 代理方式实现前后置log
 */
public class ArithmeticCalculatorLoggingProxy {

    //要代理的对象
    IArithmeticCalculator mTarget;

    public ArithmeticCalculatorLoggingProxy(IArithmeticCalculator target) {
        this.mTarget = target;
    }

    //返回一个日志代理
    public IArithmeticCalculator getLoggingProxy() {
        IArithmeticCalculator proxy;

        //代理对象由哪一个类加载器负责加载
        ClassLoader classLoader = mTarget.getClass().getClassLoader();
        //代理对象的类型,即其中有哪些方法
        Class[] interfaces = new Class[]{IArithmeticCalculator.class};
        //当调用代理对象其中的方式时,该执行的代码
        InvocationHandler invocationHandler = new InvocationHandler() {
            /**
             * @param proxy 正在返回的代理对象
             * @param method 正在被调用的方法
             * @param args 调用方法时传入的参数
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //前置日志
                System.out.println("The method " + method.getName() + " begin with " + Arrays.asList(args));
                //执行方法
                Object result = method.invoke(mTarget, args);
                //后置日志
                System.out.println("The method " + method.getName() + " end with " + result);
                return result;
            }
        };
        proxy = (IArithmeticCalculator) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);

        return proxy;
    }
}

测试:

  @org.junit.Test
    public void testLoggingProxy() {

        IArithmeticCalculator target = new ArithmeticCalculatorLoggingImplSimple();
        IArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy();

        int resultAdd = proxy.add(1, 3);
        System.out.println("--->" + resultAdd);

        int resultMul = proxy.mul(2, 3);
        System.out.println("--->" + resultMul);
    }

3AspectJ

  • 要在 Spring 中声明 AspectJ 切面, 只需要在 IOC 容器中将切面声明为 Bean 实例。当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理。
  • 在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类。
  • 通知是标注有某种注解的简单的 Java 方法。
  • AspectJ 支持 5 种类型的通知注解:

@Before: 前置通知, 在方法执行之前执行

@After: 后置通知, 在方法执行之后执行

@AfterRunning: 返回通知, 在方法返回结果之后执行

@AfterThrowing: 异常通知, 在方法抛出异常之后

@Around: 环绕通知, 围绕着方法执行

Spring使用AOP框架Aspectj

1 引入依赖

<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.8.RC1</version>
</dependency>

2 在配置文件中加入aop和context命名空间

注意schemaLocation下引入的aop、context配置不能少,不然会报错。

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

3 在Bean配置文件中加以下配置

<!--配置要自动扫描的包-->
    <context:component-scan base-package="site.exciter.spring.aop.impl"/>

    <!--使aspectj注解起作用:自动为匹配的类生成对象-->
    <aop:aspectj-autoproxy/>

4 接口类

package site.exciter.spring.aop.impl;

public interface IArithmeticCalculator {
    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}

5 实现类

package site.exciter.spring.aop.impl;

import org.springframework.stereotype.Component;

@Component("arithmeticCalculator")
public class ArithmeticCalculatorImpl implements IArithmeticCalculator {

    @Override
    public int add(int i, int j) {
        return i + j;
    }

    @Override
    public int sub(int i, int j) {
        return i - j;
    }

    @Override
    public int mul(int i, int j) {
        return i * j;
    }

    @Override
    public int div(int i, int j) {
        return i / j;
    }
}

6 创建切面

package site.exciter.spring.aop.impl;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

/**
 * @Aspect当前类声明成一个切面
 */
@Component
@Aspect
public class LoggingAspect {

    /**
     * @param point 链接信息
     * @Before声明该方法是一个前置方法:在目标方法开始执行之前执行
     *
     * execution(* site.exciter.spring.aop.impl. * . * ( int, int))
     * 第一个*表示方法修饰符和返回值可以为任意类型,第二个*代表该包下所有的类,第三个*表示所有的方法
     * JoinPoint可以访问链接细节:方法和参数等
     */
    @Before("execution(* site.exciter.spring.aop.impl.*.*(int,int))")
    public void beforeMethod(JoinPoint point) {
        String methodName = point.getSignature().getName();
        List<Object> args = Arrays.asList(point.getArgs());
        System.out.println("The method " + methodName + " begin with " + args);
    }
}

7 测试

package site.exciter.spring.aop.impl;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {

    @org.junit.Test
    public void testImpl() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        IArithmeticCalculator calculator = (IArithmeticCalculator) context.getBean("arithmeticCalculator");
        int result = calculator.add(3, 6);
        System.out.println("result-->" + result);
    }
}

8 Log

The method add begin with [3, 6]
result-->9

相关术语:

切面(Aspect): 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象。

通知(Advice): 切面必须要完成的工作。

目标(Target): 被通知的对象。

代理(Proxy): 向目标对象应用通知之后创建的对象。

连接点(Joinpoint): 程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为 ArithmethicCalculator#add(); 方位为该方法执行前的位置。

切点(pointcut): 每个类都拥有多个连接点:例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

关注木水小站 (zhangmushui.cn)和微信公众号【木水Code】,及时获取更多最新技术干货。