Spring-aop面向切面

Posted 超*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring-aop面向切面相关的知识,希望对你有一定的参考价值。

1、理解必要的专业术语

 先看看上面图,这是我的个人理解。(画的丑,主打真实)

        1)Advice,通知/增强:类方法中提出来的共性功能(大白话就是提出来的重复代码)
        2)Pointcut,切入点/切点:通知返回的方法
        3)连接点:方法
        4)织入:运行时通知插入到方法的过程
        5)aspect,切面:通知功能在什么时候插入到切入点(通知和切入点的结合)
        6)目标对象:被增强的对象
        7)代理:应用切面的过程(AOP框架使用代理模式创建对象,实现在连接处插入增强)

2、代码练习理解

        1)创建两个类

package com.gc;

public class sy 
    public void a1()
//        System.out.println("Ac is running ... ");    //共性功能
        System.out.println("a1 is running...");
    

    public void a2()
//        System.out.println("Ac is running ... ");
        System.out.println("a2 is running ...");
    

package com.gc;

public class AAdvice 
    public void Ac()
        System.out.println("Ac is running ... ");
    

        2)applicationContext.xml配置

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

    <bean id="sy" class="com.gc.sy"/>
    <bean id="aopAdvice" class="com.gc.AAdvice"/>
    <aop:config>
        <!--配置切入点-->
        <aop:pointcut id="pt" expression="execution(* *..*.*(..))"/>
        <!--配置切面-->
        <aop:aspect ref="aopAdvice">
            <aop:before method="Ac" pointcut-ref="pt"/>
        </aop:aspect>
    </aop:config>
</beans>

切点表达式:
 execution(返回类型 包名.方法名(参数类型)),*:就是所有
*..*,比如包名叫com.gc.hh,正常逻辑写是*.*.*,简写就是*..*

        3)测试

package com.bdqn.test;

import com.bdqn.sy;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AAtest 
    @Test
    public void test()
        ApplicationContext cxt = new ClassPathXmlApplicationContext("applicationContext.xml");
        sy sy=(com.bdqn.sy) cxt.getBean("sy");
        sy.a1();
    

 可以看到增强方法成功插入到切入点了。

这时候肯定有同学问,为什么是插入到前面?注意看applicationContext.xml配置中的aop:before,这是前置增强。

3、Aop五种增强处理
        1)前置增强(aop:before):上面代码已经试过了,就是目标方法前织入增强处理
        2)最终增强(aop:after):无论目标是否抛出异常,方法最后都会织入增强处理
        3)后置增强(aop:after-returning):目标方法正常运行后织入增强处理
        4)异常增强(aop:after-throwing):目标方法抛出异常后织入增强处理
        5)环绕增强(aop:around):目标方法前后都可以织入增强处理

这里演示一下环绕增强,解决一下疑惑。其他增强处理自己动手试一试。

在上面的代码基础上修改applicationContext.xml:

  <!--配置切面-->
        <aop:aspect ref="aopAdvice">
            <aop:around method="Ac" pointcut-ref="pt"/>
        </aop:aspect>

运行测试类: 

 显示出来增强方法的内容,那切入点方法的内容去哪了?        加入一行代码

package com.bdqn;

import org.aspectj.lang.ProceedingJoinPoint;

public class AAdvice 
    public void Ac(ProceedingJoinPoint joinPoint) throws Throwable 
        System.out.println("around_before");
        //调用原始方法
        joinPoint.proceed();
        System.out.println("Ac is running ... ");
    

 可以看到,在调用原始方法前的内容会插入到切入点方法内容的前面,反之。

Spring-AOP

AOP

AOP面向切面编程

动态代理

实现方式:jdk动态代理,使用jdk中的Proxy,Method ,InvocationHanderl创建代理对象

​ jdk动态代理要求目标类必须实现接口

cglib动态代理:第三方的工具库,创建代理对象,原理是继承,通过继承目标类,创建子类。

​ 子类就是代理对象。要求目标类不能时final的,方法也不能是final的

动态代理的作用

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

2)减少代码的重复

3)专注业务逻辑代码

4)解耦合,让你的业务功能和日志,事务非业务功能分离。

实例

接口类

public interface SomeService 
    void doSome();
    void doOther();

实现类

public class SomeServiceImpl implements SomeService 
    @Override
    public void doSome() 
        System.out.println("执行业务方法doSome");
    

    @Override
    public void doOther() 
        System.out.println("执行业务方法doOther");
    

service类的代码不修改,也能够增加 输出时间,事务

serviceTools类

public class serviceTools 

    public static void doLog()
        System.out.println("非业务方法,方法的执行时间"+ new Date());
    
    public static void doTrans()
        System.out.println("非业务方法,方法执行完毕后,提交事务");
    


MyIncationHandler类

public class MyIncationHandler implements InvocationHandler 
    private Object target;   //SomeServiceImpl类

    //目标对象
    public MyIncationHandler(Object target) 
        this.target = target;
    

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
        //通过代理对象执行方法时,会调用执行这个invoke();
        Object res=null;
        serviceTools.doLog();//工具类中的时间方法
        //执行目标类的方法,通过Method类实现
        res= method.invoke(target, args);//SomeServiceImpl.doOther(),doSome()
        serviceTools.doTrans();//工具类中的事务方法
        return res;
    

通过构造器将要代理的对象传进来

method是要代理的方法

测试类

public class MyApp 
    public static void main(String[] args) 

        //使用jdk的proxy创建代理对象
        //创建目标对象
        SomeService target =new SomeServiceImpl();
        //创建InvocationHandler对象
        InvocationHandler handler=new MyIncationHandler(target);
        //创建SomeService接口的所有实现类
        Class[] classes=SomeService.class;
        //创建代理对象
        SomeService o = (SomeService) Proxy.newProxyInstance(target.getClass().getClassLoader(), classes, handler);
        //调用方法
        o.doOther();
        o.doSome();
        

Proxy.newProxyInstance方法:
第一个参数是目标对象的Loader

第二个参数是目标接口的所有实现类

第三个对象是代理类

扩展

想要doOther方法有时间和事务,而doSOme方法没有

需要修改代理类的invoke方法

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
        //通过代理对象执行方法时,会调用执行这个invoke();
        Object res=null;
        if (method.getName().equals("doOther"))


        serviceTools.doLog();
        //执行目标类的方法,通过Method类实现
        res= method.invoke(target, args);//SomeServiceImpl.doOther(),doSome()
        serviceTools.doTrans();
        else 
            res=method.invoke(target,args);
        
        return res;
    

因为method是执行的方法,想要只有doOther方法有时间和事务,则需method.getName().equals(“doOther”)进行一个判断即可

什么是AOP

AOP简介

AOP(Aspect Orient Programming),面向切面编程,面向切面编程时从动态角度考虑程序运行过程。

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

AOP就是动态代理的规范化,把动态代理的实现步骤,方式都定义好了,让开发人员用一种统一的方式,使用动态代理

AOP(Aspect Orient Programming),面向切面编程

Aspect:切面,给你的目标类增加的功能,就是切面,像上面用的时间,事务就是切面

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

Orient:面向,对这。

Programming:编程

OOP:面向对象编程

怎么理解面向切面编程

1)需要在分析项目功能时,找出切面

2)合理的安排切面的执行时间(在目标方法前,还是目标方法后)

3)合理的安排切面执行的位置,在哪个类,哪个方法增加增强功能

术语:

1)Aspect:切面,表示增强的功能,就是一推代码,完成某个一个功能。非业务功能。

​ 常见的切面功能有日志,事务,统计信息,参数检查,权限验证。

2)JoinPoint:连接点,连接业务方法和切面的位置。就某类中的业务方法

3)Pointcut:切入点,指多个连接点方法的集合。

4)目标对象:给哪个类的方法增加功能,这个类就是目标对象

5)Advice:通知,通知表示切面功能执行的时间。

说一个切面有三个关键的要素:

1)切面的功能代码,切面干什么

2)切面的执行位置,使用Pointcut表示切面执行的位置

3)切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后。

AOP的实现

aop是一个规范,时动态代理的一个规范化,一个标准

aop的技术实现框架:
1、Spring:Spring在内部实现了aop规范,能做aop的工作

​ SPring主要在事务处理时使用aop。

​ 我们项目开发中很少使用spring的aop实现,因为spring的aop比较笨重

2、aspectJ:一个开源的专门做aop的框架。spring框架中继承了aspectj框架,通过spring就能使用aspectj的功能。

​ aspectj框架实现aop有两种方式:

​ 1、使用xml的配置文件

​ 2、使用注解,我们在项目中要做zop功能,一般都是用注解,aspectj有5个注解。

aspectj框架

切面的执行时间

1)切面的执行时间,这个执行时间在规范中叫做advice(通知,增强)

​ 在aspectj框架中使用注解表示的。也可以使用xml配置文件中的标签

​ 1)@Before

​ 2)@AfterReturning

​ 3)@Around

​ 4)@AfterThrowing

​ 5)@After

2)表示切面执行的位置,使用的是切入点表达式

AspectJ的切入点表达式

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

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

解释:

modifiers-pattern:访问权限类型 例如:public

ret-type-pattern:返回值类型 例如:void

declaring-type-pattern:包名类名 可以没有

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

throws-pattern:抛出异常类型

?表示可选的部分

以上表达式共4个部分。

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

pattern的地方可以使用通配符

符号意义
*0至多个任意字符
用在方法参数中,表示任意多个参数
用在包名后,表示当前包及其子包路径
+用在类名后,表示当前类及其子类
用在接口后,表示当前接口及其实现类

举例:

execution(public * *(..))

指定切入点为:任意公共方法。

execution(* set*(..))

指定切入点为:任何一个以“set”开始的方法。

execution(* com.xyz.service.*.*(..))

指定切入点为:定义在service包里的任意类的任意方法

execution(* com.xyz.service..*.*(..))

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

execution(* *..service.*.*(..))

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

使用aspectj实现aop的基本步骤

1、新建maven项目
2、加入依赖
1)spring依赖
2)aspectj依赖
3)junit单元测试
3、创建目标类:接口和他的实现类。
要做的是给类中的方法增加功能

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

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

6、创建测试类,从spring容器中获取目标对象(实际就是代理对象)。
通过代理执行方法,实现aop的功能增强

实例

给doSOme方法增加一个功能,在doSome()执行之前,输出方法的执行时间

接口类

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()");
    

@Aspect
public class MyAspect 
    @Before(value = "execution(public void com.hr.qjb.ba01.SomeServiceImpl.doSome(String,Integer))")//参数为切入点表达式
    public void myBefore()
        //就是你切面要执行的功能代码
        System.out.println("前置通知,切面功能:在目标方法之前输出执行时间"+new Date());
    

Aspect

Aspect:是aspectj框架中的注解。
作用:表示当前类是切面类
切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
位置:在类定义的上面

定义方法

​ 定义方法,方法是实现切面功能的。
​ 方法的定义要求:
​ 1、公共方法public
​ 2、方法没有返回值
​ 3、方法名称自定义
​ 4、方法可以有参数,也可以没有参数
​ 如果有参数,参数不是自定义的,有几个参数类型可以使用。
​ @Before:表示前置通知注解
​ 属性:value,是切入点表达式,表示切面的功能执行的位置
​ 位置:在方法的上面

特点:1、在目标方法之前先执行的

2、不会改变目标方法的执行结果

3、不会影响目标方法的执行。

配置文件

<?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.hr.qjb.ba01.SomeServiceImpl"/>

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

<!--    声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。
        创建代理对象是在内存中实现的,修改目标对象的内存中的结构。创建为代理对象
        所以目标对象就是被修改后的代理对象
        aspectj-autoproxy:会把spring容器中的所有目标对象,一次性都生成代理对象
    -->
    <aop:aspectj-autoproxy/>
</beans>

首先测试类会去先去解析xml配置文件,首先找<bean id="someService" class="com.hr.qjb.ba01.SomeServiceImpl"/>这段代码,因为里面只是一个实现类,一个doSome方法,在去找 <bean id="myAspect" class="com.hr.qjb.ba01.MyAspect" />这段代码,找到以后因为MyAspect是被@Aspect注解的,所以被认为是切面类,后来在往下执行发现@Before(value = "execution(public void com.hr.qjb.ba01.SomeServiceImpl.doSome(String,Integer))")这个注解,参数指定的是目标对象,就是SomeServiceImpl这个类中的doSome方法,就去会实现代理,在这个方法的前面执行切面要执行的功能代码

切入表达式的多种写法

最为准确的写法:准确锁定到SomeServiceImpl类中的doSome方法

    @Before(value = "execution(public void com.hr.qjb.ba01.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore()
        //就是你切面要执行的功能代码
        System.out.println("前置通知,切面功能:在目标方法之前输出执行时间"+new Date());
    

省略访问修饰符写法:将public 省略

    @Before(value = "execution( void com.hr.qjb.ba01.SomeServiceImpl.doSome(String,Integer))")
    public void myBefore()
        //就是你切面要执行的功能代码
        System.out.println("1====前置通知,切面功能:在目标方法之前输出执行时间"+new Date());
    

省略访问修饰符并利用通配符是所有包中的SomeServiceImpl类中的doSome的方法

    @Before(value = "execution( void *..SomeServiceImpl.doSome(String,Integer))")
    public void myBefore()
        //就是你切面要执行的功能代码
        System.out.println("2====前置通知,切面功能:在目标方法之前输出执行时间"+new Date());
    

所有返回值类型以及利用通配符是所有包中的SomeServiceImpl类中的doSome的方法

@Before(value = "execution( * *..SomeServiceImpl.doSome(..))")
public void myBefore()
    //就是你切面要执行的功能代码
    System.out.println("3====前置通知,切面功能:在目标方法之前输出执行时间"+new Date());

还可以定义多个前置通知

    @Before(value = "execution( * *..SomeServiceImpl.do*(..))")
    public void myBefore()
        //就是你切面要执行的功能代码
        System.out.println("4====前置通知,切面功能:在目标方法之前输出执行时间"+new Date());
    

    @Before(value = "execution( * *..SomeServiceImpl.do*(..))")
    public void myBefore2()
        //就是你切面要执行的功能代码
        System.out.println("5====前置通知,切面功能:在目标方法之前输出执行时间"+new Date());
    

JoinPoint

写在切面类中的通知方法的参数里,可以获得业务方法中的参数以及一些方法的信息

getSignature方法:获取方法的签名(定义)

getSignature.getName方法:获取方法的名称

getArgs方法:获取方法中的参数,返回的是一个Object的数组

@AfterReturning

定义方法

后置通知定义方法,方法是实现切面功能的。

方法的定义要求:

1、公共方法public

2、方法没有返回值

3、方法名称自定义

4、方法有参数的

如果有参数,参数不是自定义的,有几个参数类型可以使用。

@AfterReturning:后置通知

属性:1、value 切入点表达式

2、returning 自定义的变量,表示目标方法的返回值的。

例如:

自定义变量名必须和通知方法的形参名一样

位置:在方法定义的上面

@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther2(..))",
        returning = "res")

特点:

1、在目标方法之后执行的

2、能够获取目标方法的返回值,可以根据这个返回值做不同的处理功能

3、可以修改这个返回值

环绕通知

环绕通知方法的定义格式

1、public

2、必须有一个返回值,推荐使用Object

3、方法名称自定义

4、方法有参数,固定的参数ProceedingJoinPoint

例如

public Object myAround(ProceedingJoinPoint pjp)


@Around:环绕通知

属性:value:切入点表达式

​ 位置:在方法的定义上面

例如

@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")

特点:
1、它是功能最强的通知

2、在目标方法的前和后都能增强功能

3、控制目标方法是否被调用执行

4、修改原来的目标方法的执行结果。影响最后的调用结果

环绕通知,等同于jdk动态代理的,InvocationHandler接口

参数:ProceedingJoinPoint 就等同于 method

作用:执行目标方法的

返回值:就是目标方法的执行结果,可以被修改。

实例

接口类方法

  @Override
    public String doFirst(String name, Integer age) 
        System.out.println("业务方法doFirst()");
        return "doFirst";
    

切面类

    @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable 
        //实现环绕通知
        Object result=null;
        //1、目标方法调用
        System.out.println("环绕通知"+new Date());
        result = pjp.proceed();//method.invoke();  Objcet result = doFirst();
        System.out.println("环绕通知,在目标方法之后,提交事务");
        //2、在目标方法的前或者后加入功能

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

测试类

    @Test
    public void ttest01()
        String config="applicationContext.xml";
        ApplicationContext ctx=new ClassPathXmlApplicationContext(config);
        //从容器中获取目标对象
        SomeService proxy  = (SomeService)ctx.getBean("someService");
        //通过代理的对象执行方法,实现目标方法执行时,增强了功能
        String s = proxy.doFirst("才", 20);//执行myAround()
        System.out.println(s);
    

因为ProceedingJoinPoint继承了JoinPoint,所以可以同ProceedingJoinPoint获得方法中的参数,从参数上进行判断,事务的处理

测试类中的proxy.doFirst方法,也就相当于是去执行了切面类中的环绕方法

环绕通知:经常做事务,在目标方法之前开启事务,执行目标方法,在方法之后提交事务

@AfterThrowing

异常通知方法的定义格式

1、public

2、没有返回值

3、方法名称自定义

4、方法有一个Exception参数,如果还有就是JoinPoint

例如:

public void myAfterThrowing(Exception ex)


@AfterThrowing:异常通知

​ 属性:1、value:切入点表达式

​ 2、throwing:自定义变量,表示目标方法抛出的异常对象。

​ 变量名必须和方法的参数名一样

​ 特点:

​ 1、在目标方法抛出异常时执行的

​ 2、可以做异常的监控程序,监控目标方法执行时是不是又异常

​ 如果有异常,可以发送邮件,短信进行通知

  try
   SomeServiceImpl.doSecond(..)
 catch(Exception e)
   myAfterThrowing(e)
 

如果没有发生异常则正常执行业务方法,如果发生了异常,去执行切面类中的方法

@After

最终通知方法的定义格式

1、public

2、没有返回值

3、方法名称自定义

4、方法没有参数,如果还有时JoinPoint

@After:最终通知

属性:value 切入点表达式

位置:在方法的上面

特点:

1、它总是会执行

2、在目标方法之后执行的。

实例

切面类

@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter()
    System.out.println("执行最终通知,总是会被执行的代码");
    //一般做资源清楚工作的

@Pointcut

@Pointcut:可以定义和管理切入点,如果你的项目中有多个切入点表达式是重复的,可以复用的。可以使用@Pointcut

​ 属性:value 切入点表达式

位置:在自定义的方法上面

特点:

当我们使用@Pointcut定义在一个方法的上面,此时这个方法的名称就是切入点表达式的别名。

其他的通知中,value属性就可以使用这个方法名称,代替切入点表达式了

@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void mypt()
    //无需代码,

测试

@After(value = "mypt()")
    public void myAfter()
        System.out.println("执行最终通知,总是会被执行的代码");
        //一般做资源清楚工作的
    

会被执行

注意:不要忘记加括号,因为是方法,一般为私有

框架的代理分类

目标类没有接口,使用cglib动态代理,spring框架会自动应用cglib

如果你期望目标类有接口,使用cglib代理

配置文件中的加入

<aop:aspectj-autoproxy proxy-target-class="true"/>

设置proxy-target-class="true"为真,表明要强制使用cglib接口

以上是关于Spring-aop面向切面的主要内容,如果未能解决你的问题,请参考以下文章

Spring-AOP面向切面编程

Spring-AOP(面向切面编程)

spring-AOP(面向切面编程)

Spring-aop面向切面

08-SPRING-AOP

spring-AOP概念