Java--Spring之AOP面向切面编程

一、JDK动态代理

具体可参考之前博文 Java--JDK动态代理(AOP)

在这里只做简单说明

动态代理是指代理类在程序运行时进行创建的代理方式。这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据Java代码中的“指示”动态生成的。(动态代理中的代理类并不要求在编译期就确定,而是可以在运行期动态生成,从而实现对目标对象的代理功能)

相比于静态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数

动态代理又分为两种方式

(1)JDK动态代理:

使用java反射包中的类和接口实现动态代理的功能。反射包 java.lang.reflect , 里面有三个类 : InvocationHandler , Method, Proxy

(2)Cglib动态代理:

一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。 通过继承目标类,在子类中重写父类同名方法,实现功能修改(重写的方法不能是final)

JDK原生动态代理

JDK原生动态代理主要是使用Java中的反射机制,使用了反射包 java.lang.reflect下的三个类

InvocationHandler , Method, Proxy

JDK动态代理步骤

(1)创建接口,定义目标类要完成的功能

(2)创建目标类实现接口

(3)创建InvocationHandler接口的实现类,在invoke方法中完成代理类的功能(调用目标方法,增强功能)

(4)使用Proxy类的静态方法,创建代理对象。 并把返回值转为接口类型

如下创建一个MyInvocationHandler

public class MyInvocationHandler implements InvocationHandler {

    //目标对象
    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object res = null;

        System.out.println("执行了InvocationHandler中的invoke()方法:" + method.getName());

        String methodName = method.getName();
        if ("doSome".equals(methodName)){
            //在目标方法之前,输出日志时间
            ServiceTools.doLog();

            //执行目标类的方法,通过Method类实现
            res = method.invoke(target,args);

            //提交事务
            ServiceTools.doTransaction();
        }else {
            res = method.invoke(target,args);
        }

        return res;
    }
}
public class TestApp {
    public static void main(String[] args) {
        System.out.println("========== 原始方法new对象创建 ==========");
        SomeService someService = new SomeServiceImpl();
        someService.doSome();;
        someService.doOther();


        System.out.println("========== JDK动态代理创建代理对象,实现方法 ==========");
        /**
         使用JDK代理创建代理对象,以及实现方法
         * */
        //创建目标对象
        SomeService target = new SomeServiceImpl();

        //创建InvocationHandler对象
        InvocationHandler handler = new MyInvocationHandler(target);

        //使用Proxy创建代理
        SomeService proxy = (SomeService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                handler);
        //com.sun.proxy.$Proxy0
        System.out.println("proxy=="+proxy.getClass().getName());
        //通过代理方法执行方法,调用invocationHandler中的invoke方法
        proxy.doSome();
        proxy.doOther();
    }
}

动态代理的作用

1、在目标类源代码不改变的情况下,增加功能

2、减少重复代码,专注业务逻辑代码

3、解耦合,让业务功能和日志,事务等非业务功能分离

二、AOP

AOPAspect Orient Programming),面向切面编程

面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到 主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。

AOP 是 Spring 框架中的一个重要内容。利用 AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP 底层,就是采用动态代理模式实现的;采用了两种代理JDK 动态代理CGLIB动态代理

1、Aspect:切面,泛指交叉业务逻辑,给目标类增加的功能,就是切面。如日志,事务等都是切面。常用的切面 是通知(Advice)。实际就是对主业务逻辑的一种增强

切面的特点: 一般都是非业务方法,独立使用

2、JoinPoint:连接点,指可以被切面织入的具体方法。通常业务接口中的方法均为连接点

3、Pointcut:切入点,指声明的一个或多个连接点的集合。通过切入点指定一组方法

被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强

4、Target:目标对象,指 将 要 被 增 强 的 对 象 。 即 包 含 主 业 务 逻 辑 的 类 的 对 象

5、Advice:通知,表示切面的执行时间,Advice 也叫增强通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。 切入点定义切入的位置,通知定义切入的时间

AOP 是一种编程思想,很多框架都进行了实现,Spring就是其中之一

在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式

AspectJ :AspectJ 是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现

AspetJ 是 Eclipse 的开源项目,其官网地址http://www.eclipse.org/aspectj/

三、AspetJ

(一)AspetJ切入点 表达式

AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是

execution(modifiers-pattern? ret-type-pattern 
declaring-type-pattern?name-pattern(param-pattern)
 throws-pattern?)

modifiers-pattern] 访问权限类型

ret-type-pattern 返回值类型

declaring-type-pattern 包名类名

name-pattern(param-pattern) 方法名(参数类型和参数个数)

throws-pattern 抛出异常类型

?表示可选的部分

以上表达式共 4 个部分

execution(访问权限 方法返回值 方法声明(参数) 异常类型)

如下:

execution(public * *(..)) 
指定切入点为:任意公共方法

execution(* set*(..)) 
指定切入点为:任何一个以“set”开始的方法

execution(* com.xx.service.*.*(..)) 
指定切入点为:定义在 service 包里的任意类的任意方法

execution(* com.xx.service..*.*(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法
“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类

execution(* *..service.*.*(..))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点

execution(* *.service.*.*(..))
指定只有一级包下的 serivce 子包下所有类(接口)中所有方法为切入点

(二)AspetJ通知注解

(1)前置通知(2)后置通知(3)环绕通知(4)异常通知(5)最终通知

使用aspectj实现aop步骤:

1、新建maven项目

2、加入依赖 (1)spring依赖 (2)aspectj依赖  pom.xml文件

<dependencies>

        <!-- 单元测试依赖 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

        <!--spring依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>

        <!--aspectj依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>

    </dependencies>

3、创建目标类:接口和其实现类(给类中方法增加功能)

public interface SomeService {
    void doSome(String name,Integer age);
}

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome(String name, Integer age) {
        System.out.println("====目标方法doSome()====");
    }

    public void doOther(String name,Integer age) {
        System.out.println("====目标方法doOther()====");
    }
}

4、创建切面类 (1)在类上面加 @Aspect注解 (2)在类中定义方法,方法就是切面要执行的功能代码 在方法上加入aspectj的通知注解,如@Before,有切入点表达式execution()

5、创建spring配置文件:声明对象,把对象交给容器统一管理 (1)声明目标对象 (2)声明切面类对象 (3)声明aspectj框架中的自动dialing生成器标签 自动代理生成器:用来完成代理对象的自动创建功能

spring主配置文件applicationContext.xml文件

<aspectj-autoproxy>:会把spring容器中所有的目标对象,一次性都生成代理对象

声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象;创建代理对象是在内存中实现的,修改目标对象的内存结构,创建为代理对象,目标对象就是被修改后的代理对象

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

    <!-- 讲对象交给spring容器,由spring容器统一创建,管理对象 -->
    <!-- 声明目标对象 -->
    <bean id="someService" class="com.xx.SomeServiceImpl" />

    <!-- 声明切面类对象 -->
    <bean id="myAspect" class="com.xx.MyAspect" />

    <!-- 声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象
        创建代理对象是在内存中实现的,修改目标对象的内存结构,创建为代理对象
        目标对象就是被修改后的代理对象

        aspectj-autoproxy:会把spring容器中所有的目标对象,一次性都生成代理对象
     -->
    <aop:aspectj-autoproxy />

</beans>

加入 <aop:aspectj-autoproxy /> 标签之后spring配置文件变化如下

 

6、创建测试类 

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

        //从容器中获取目标对象
        SomeService someService = (SomeService) ac.getBean("someService");

        //proxy:com.sun.proxy.$Proxy8
        System.out.println("proxy:" + someService.getClass().getName());

        //通过代理对象执行方法,实现目标方法执行时,增强功能
        someService.doSome("wang",18);
    }

1、前置通知

/**
 @Aspect :是aspectj框架中的注解
 作用:表示当前类是切面类
 切面类:用来给业务方法增加功能的类,这个类中有切面的功能代码
 位置:在类定义上面
 */
@Aspect
public class MyAspect {
    /**
     定义方法,方法是实现切面功能的
     方法定义要求
        1、公共方法  public
        2、方法没有返回值
        3、方法名称自定义
        4、方法可以有参数(写参数类型),也可以无参
     */

    /**
     @Before :前置通知注解
        属性:value,切入点表达式,表示切面功能执行的位置
        位置:在方法上面
        特点:
           (1)在目标方法之前执行
           (2)不会改变目标方法的执行结果
           (3)不影响目标方法的执行
     */
    //访问修饰符,返回值类型,包名类名,方法名,形参列表(参数类型,参数个数)
    //Shift + Ctrl + / 多行注释;Ctrl + / 单行注释
    @Before(value = "execution(public void com.mycompany.p1before.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore(){
        //切面执行的功能代码
        System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:" + new Date());
    }

    //省略修饰符
    @Before(value = "execution(void com.mycompany.p1before.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore1(){
        //切面执行的功能代码
        System.out.println("前置通知1,切面功能:在目标方法之前输出执行时间:" + new Date());
    }

    //省略包名;*.. 表示包、子包下 ;*. 表示一级包
    @Before(value = "execution(void *..SomeServiceImpl.doSome(String,Integer))")
    public void myBefore2(){
        System.out.println("前置通知2,切面功能:在目标方法之前输出执行时间:" + new Date());
    }

    //可使用 * 或者 *. 和 *.. 代替修饰符,返回值,包名方法名,参数等
    @Before(value = "execution(* *..SomeServiceImpl.*(..))")
    public void myBefore3(){
        System.out.println("前置通知3,切面功能:在目标方法之前输出执行时间:" + new Date());
    }

    @Before(value = "execution(* do*(..))")
    public void myBefore4(){
        System.out.println("前置通知4,切面功能:在目标方法之前输出执行时间:" + new Date());
    }
}

2、后置通知

@Aspect
public class MyAspect {
    /**
     后置通知定义方法,方法是实现切面功能
     方法定义要求
        1、公共方法  public
        2、方法没有返回值
        3、方法名称自定义
        4、方法有参数的,推荐Object ,参数名自定义
     */

    /**
     @AfterReturning :后置通知
        属性:
        (1)value    切入点表达式
        (2)returning    自定义的变量,表示目标方法的返回值
            自定义变量名必须和通知方法的形参名一致
        位置:在方法定义上方
        特点:
        (1)在目标方法之后执行
        (2)能够获取目标方法的返回值,根据这个返回值做不同功能处理
        (3)可修改这个返回值

        后置通知执行
            Object res = doOther();
            参数传递:传值,传引用
            myAfterReturning(res);
     */
    //访问修饰符,返回值类型,包名类名,方法名,形参列表(参数类型,参数个数)
    //Shift + Ctrl + / 多行注释;Ctrl + / 单行注释
    @AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
                    returning = "res")
    public void myAfterReturning(JoinPoint joinPoint,Object res){
        //Object res:目标方法执行后的返回值,根据返回值做切面功能处理
        System.out.println("后置通知:方法的定义"+ joinPoint.getSignature());
        System.out.println("后置通知:在目标方法之后执行的,获取的返回值是:"+res);

        if ("abcd".equals(res)){
        }

        if (res != null){
            res = "Hello Aspectj";
        }
    }
}

3、环绕通知

注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值

@Aspect
public class MyAspect {
    /**
     环绕通知
     方法定义要求
        1、公共方法  public
        2、必须有一个返回值,推荐Object
        3、方法名称自定义
        4、方法有参数且固定, ProceedingJoinPoint
     */

    /**
     @Around :环绕通知
        属性: value 切入点表达式
        位置:方法定义上面
        特点:
        (1)功能最强的通知
        (2)在目标方法的前和后都能增强功能
        (3)控制目标方法是否被调用执行
        (4)修改原来的目标方法的执行结果,影响调用结果
     环绕通知,等同于JDK动态代理的InvocationHandler接口
        参数:ProceedingJoinPoint 等同于 Method;作用:执行目标方法
        返回值:目标方法的执行结果,可以被修改
     环绕通知: 经常做事务, 在目标方法之前开启事务,执行目标方法, 在目标方法之后提交事务
     */
    @Around(value = "execution(* *..SomeServiceImpl.doAround(..))")
    public Object myAround(ProceedingJoinPoint pjp){
        String name = "";
        //获取第一个参数
        Object args[] = pjp.getArgs();
        if (args != null && args.length > 1){
            Object arg = args[0];
            name = (String) arg;
        }

        //实现环绕通知
        Object result = null;
        System.out.println("环绕通知:在目标方法之前,输出时间:"+ new Date());

        //1、目标方法调用
        if ("wang".equals(name)){
            //符合条件,执行目标方法
            try {
                result = pjp.proceed();//method.invoke();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }

        System.out.println("环绕通知:在目标方法之后,提交事务");

        //2、在目标方法前或后加入功能
        if (result != null){
            //修改目标方法的执行结果,影响最后的调用结果
            result = "Hello AspectJ AOP";
        }

        //返回目标方法的执行结果
        return result;
    }
}

4、异常通知

public void doException(String name,Integer age) {
        System.out.println("====目标方法doException()====" + 10/0);
    }
@Aspect
public class MyAspect {
    /**
     异常通知
     方法定义要求
        1、公共方法  public
        2、没有返回值
        3、方法名称自定义
        4、方法有一个Exception,如果还有是JoinPoint
     */

    /**
     @AfterThrowing :异常通知
        属性:
        (1)value 切入点表达式
        (2)throwing 自定义的变量,表示目标方法抛出的异常对象;变量名和方法参数名必须一致
        特点:
        (1)在目标方法抛出异常时执行
        (2)做异常的监控程序,监控目标方法执行时是不是有异常;如有异常,可发送邮件、短信通知

        执行过程
        try{
            doException(..);
        }catch(Exception e){
            myAfterThrowing(e);
        }
     */
    @AfterThrowing(value = "execution(* *..SomeServiceImpl.doException(..))",
                    throwing = "ex")
    public void myAfterThrowing(Exception ex){
        System.out.println("异常通知:方法发生异常时,执行:" + ex.getMessage());
        //发送邮件,短信,通知开发人员
    }

5、最终通知

无论目标方法是否抛出异常,该增强均会被执行

public void doAfter() {
        System.out.println("====目标方法doAfter====" + 10/0);
    }
@Aspect
public class MyAspect {
    /**
     最终通知
     方法定义要求
        1、公共方法  public
        2、没有返回值
        3、方法名称自定义
        4、方法没有参数,如果有是JoinPoint
     */

    /**
     @After :最终通知
        属性:
            value 切入点表达式
        特点:
        (1)总会执行
        (2)在目标方法之后执行

        执行过程
        try{
            doAfter(..);
        }catch(Exception e){

        }finally{
            myAfter();
        }
     */
    @After(value = "execution(* *..SomeServiceImpl.doAfter(..))")
    public void myAfter(){
        System.out.println("执行最终通知,总是会被执行的代码");
    }
}

6、JoinPoint参数

@Aspect
public class MyAspect {

    /**
     JoinPoint:指定通知方法中的参数
        业务方法,要加入切面功能的业务方法
        作用:在通知方法中获取方法执行的信息,如方法名称,实参等
        JoinPoint参数的值由框架赋予,必须是第一个位置的参数
     */
    //访问修饰符,返回值类型,包名类名,方法名,形参列表(参数类型,参数个数)
    //Shift + Ctrl + / 多行注释;Ctrl + / 单行注释
    @Before(value = "execution(public void com.mycompany.p2joinpoint.SomeServiceImpl.*(String,Integer))")
    public void myBefore(JoinPoint joinPoint){
        //获取方法的完整定义
        System.out.println("方法的签名(定义):" + joinPoint.getSignature());
        System.out.println("方法的名称:" + joinPoint.getSignature().getName());

        //方法的签名(定义):void com.mycompany.p2joinpoint.SomeService.doSome(String,Integer)
        //方法的名称:doSome
        //参数:wang
        //参数:18

        //获取方法实参
        Object args[] = joinPoint.getArgs();
        for (Object arg:args){
            System.out.println("参数:" + arg);
        }

        //切面执行的功能代码
        System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:" + new Date());
    }

}

7、PointCut

如果项目中有多个切入点表达式是重复的(可复用),可使用@PointCut

@Aspect
public class MyAspect {
    /**
     @PointCut :定义和管理切入点,如果项目中有多个切入点表达式是重复的(可复用),可使用@PointCut
        属性:value 切入点表达式
        特点:
        (1)使用@PointCut定义在一个方法上面,此时这个方法的名称就是切入点表达式别名
        (2)其他通知value属性可以使用这个方法名称,代替切入点表达式
     */
    @Pointcut(value = "execution(* *..SomeServiceImpl.doPointCut(..))")
    public void myPointCut(){
    }

    @Before(value = "myPointCut()")
    public void myBefore(){
        System.out.println("前置通知,在目标方法之前先执行的");
    }

    @AfterReturning(value = "myPointCut()")
    public void myAfterReturning(){
        System.out.println("后置通知,在目标方法之后先执行的");
    }
}

8、自动应用cglib动态代理

干掉接口类,只留下普通股类

目标类没有接口,spring框架会自动应用cglib动态代理 proxy:com.xx.SomeServiceImpl$$EnhancerBySpringCGLIB$$d6c75e4a

9、设置应用cglib动态代理

修改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
       https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 讲对象交给spring容器,由spring容器统一创建,管理对象 -->
    <!-- 声明目标对象 -->
    <bean id="someService" class="com.mycompany.p8autocglib.SomeServiceImpl" />

    <!-- 声明切面类对象 -->
    <bean id="myAspect" class="com.mycompany.p8autocglib.MyAspect" />

    <!-- 声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象
        创建代理对象是在内存中实现的,修改目标对象的内存结构,创建为代理对象
        目标对象就是被修改后的代理对象

        aspectj-autoproxy:会把spring容器中所有的目标对象,一次性都生成代理对象
     -->
<!--    <aop:aspectj-autoproxy />-->

    <!--
        如果期望目标类有接口,使用cglib动态代理
        proxy-target-class="true":告诉spring框架,需要使用cglib动态代理
    -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

</beans>