Spring5(3)-面向切面编程

135 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

目录

一.概念

二.手写AOP框架

三.Spring支持的AOP的实现

1.Spring的AOP的类型 

2.AOP的常用术语

3.AspectJ框架

(1) Aspect的通知类型

(2)AspectJ的切入点表达式

(3)使用AspectJ的环境

4.前置通知

(1)前置通知流程分析

(2)前置通知切面方法开发

5.后置通知

(1)后置通知流程分析

(2)后置通知切面方法开发

6.环绕通知

(1)环绕通知执行流程分析

(2)环绕通知切面方法开发

7.最终通知

8.为一个方法添加多种通知


一.概念

AOP(Aspect Orient Programming)。

切面:公共的,通用的,重复的功能称为切面,面向切面编程就是将切面提取出来,单独开发,在需要调用的方法中通过动态代理方式进行织入。

​编辑

二.手写AOP框架

​编辑

版本1:


//图书购买业务和事务切面耦合在一起
public class BookServiceImpl {
    public void buy(){
        try{
            System.out.println("事务开启");
            System.out.println("图书购买业务功能实现");
            System.out.println("事务提交");
        }catch(Exception e){
            System.out.println("事务回滚");
        }
    }
}

​编辑

版本2:

public class BookServiceImpl {
    //todo 在父类中只有业务
    public void buy(){
        System.out.println("图书购买业务功能实现");
    }
}

//todo 子类是代理类,将父类的图书购买功能添加事务的切面
public class SubBookServiceImpl extends BookServiceImpl{
    @Override
    public void buy() {
        try{
            //todo 事务的切面
            System.out.println("事务开启");
            //todo 主业务实现
            super.buy();
            //todo 事务切面
            System.out.println("事务提交");
        }catch(Exception e){
            System.out.println("事务回滚");
        }
    }
}

​编辑

将业务和切面分开,实现了相同的功能

版本3:

静态代理,实现业务灵活切换,切面的功能在代理中体现。

​编辑

​编辑

public interface Service {
    //todo 业务功能
    void buy();
}

//todo 业务功能的具体实现
public class BookServiceImpl implements Service{
    @Override
    public void buy() {
        System.out.println("图书购买业务实现");
    }
}

public class FoodServiceImpl implements Service{
    @Override
    public void buy() {
        System.out.println("购买食品业务");
    }

/*todo 静态代理已经实现了目标对象的灵活切换,如图书购买业务,食品购买业务*/
public class Agent implements Service{
    //todo 设计成员变量为接口,为了灵活切换目标对象
    public Service target;
    //todo 使用构造方法传入目标对象
    public Agent(Service target){
        this.target = target;
    }
    @Override
    public void buy() {
        try{
            //切面功能
            System.out.println("事务开启");
            //业务功能
            target.buy();
            //切面功能
            System.out.println("事务开启");
        }catch(Exception e){
            System.out.println("事务回滚");
        }
    }
}

public class Test {
    @org.junit.Test
    public void test(){
        Agent bookAgent = new Agent(new BookServiceImpl());
        bookAgent.buy();
        Agent foodAgent = new Agent(new FoodServiceImpl());
        foodAgent.buy();
    }
}

版本4:

在静态代理中,会发现代理实现的切面功能是死的只能实现事务功能,不能灵活调用其他的切面功能比如日志权限验证功能。

有了接口使业务的功能实现更加灵活。

​编辑

 代理要实现切面接口而不是将切面接口的对象传进来。

要是传对象的话,业务接口和切面接口耦合在代理对象中,业务有变化或切面有变化,代理都得改变。

​编辑

public interface AOP {
    //default 实现类没必要实现该接口中所有的方法
    default  void before(){};

    default void after(){};

    default void exception(){};
}

public class LogAOP implements AOP{
    @Override
    public void before() {
        System.out.println("日志输出");
    }
}

public class TransactionAOP implements AOP{
    @Override
    public void before() {
        System.out.println("事务开启");
    }

    @Override
    public void after() {
        System.out.println("事务提交");
    }

    @Override
    public void exception() {
        System.out.println("事务回滚");
    }
}

/*todo 静态代理已经实现了目标对象的灵活切换,如图书购买业务,食品购买业务*/
public class Agent implements Service,AOP{
    //todo 设计成员变量为接口,为了灵活切换目标对象
    public Service target;
    public AOP aop;
    //todo 使用构造方法传入目标对象
    public Agent(Service target,AOP aop){
        this.target = target;
        this.aop = aop;
    }
    @Override
    public void buy() {
        try{
            //切面功能
            aop.before();
            //业务功能
            target.buy();
            //切面功能
            aop.after();
        }catch(Exception e){
            aop.exception();
        }
    }
}

public class Test {
    @org.junit.Test
    public void test(){
        pro4.Agent bookAgent = new pro4.Agent(new BookServiceImpl(),new TransactionAOP());
        bookAgent.buy();
        Agent foodAgent = new pro4.Agent(new FoodServiceImpl(),new LogAOP());
        foodAgent.buy();
    }
}

​编辑

 一个业务增加多个切面功能

public class Test {
    @org.junit.Test
    public void test(){
        pro4.Agent bookAgent = new pro4.Agent(new BookServiceImpl(),new TransactionAOP());
        //代理也是实现业务的一部分
        Agent bookAgent1 = new Agent(bookAgent,new LogAOP());
        bookAgent1.buy();
    }
}

​编辑

版本5:

动态代理

拆掉代理对象和业务的耦合即不再让代理对象实现业务的接口。

动态代理实现了业务功能的灵活改变,在静态代理时,要完成buy业务代理也需要实现buy方法,使用了动态代理后,代理不需要实现业务的任何方法。

public class ProxyFactory {
    public static Object getAgent(Service target,AOP aop){
        //返回生成的动态代理对象
        return Proxy.newProxyInstance(
                //类加载器
                target.getClass().getClassLoader(),
                //目标对象实现的所有的接口
                target.getClass().getInterfaces(),
                //代理功能的实现
                new InvocationHandler() {
                    @Override
                    public Object invoke(
                            //生成的代理对象
                            Object proxy,
                            //正在被调用的目标方法如buy()
                            Method method,
                            //目标方法的参数
                            Object[] args
                    ) throws Throwable {
                        Object obj = null;
                        try{
                            //切面
                            aop.before();
                            //业务
                            obj = method.invoke(target,args);
                            //切面
                            aop.after();
                        }catch(Exception e){
                            aop.exception();
                        }
                        return obj;
                    }
                });
    }
}

业务随意添加,都不会影响代理的代码

public interface Service {
    //todo 业务功能
    void buy();

    default String show(int num){return null;}
}

public class Test {
    @org.junit.Test
    public void test(){
        Service agent = (Service) ProxyFactory.getAgent(new BookServiceImpl(),new TransactionAOP());
        //agent.buy();
        String show = agent.show(5);
        System.out.println(show);
    }
}

三.Spring支持的AOP的实现

1.Spring的AOP的类型

​编辑

Advice:通知

Interceptor:拦截

2.AOP的常用术语

 ​编辑

3.AspectJ框架

AspectJ是一个优秀的切面框架,它扩展了Java语言,提供了强大的切面实现。它基于Java语言开发的,可以无缝扩展原有的功能。

对于AOP编程思想,很多框架都进行了实现,Spring就是其中之一,可以完成面向切面编程,然而AspectJ也实现了AOP的功能,且实现方式更为简捷,使用更为方便,而且还支持注解开发,所以Spring将AspectJ的对于AOP的实现引入到了自己的框架中,在Spring使用AOP开发时,一般使用AspectJ的实现方式。

(1) Aspect的通知类型

1.前置通知@Before

2.后置通知@AfterReturning

3.环绕通知@Around

事务就是环绕通知

4.最终通知@After

5.定义切入点@Pointcut(了解)

(2)AspectJ的切入点表达式

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

表达式原型

​编辑

 ​编辑

规范的公式

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

表达式简化

execution (方法返回值 方法声明(参数))

​编辑

例子

1.execution(public * *(..))

访问权限类型:public

返回值类型:*

方法声明:*

参数:..

切入点:任意公共方法

2.execution(* set*(..))

返回值类型:*

方法声明:set*

参数:..

切入点:以set开头的任意方法

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

返回值类型:*

包名类名: com.xyz.service.impl.*         这里的*是类

方法名:*

参数:..

切入点:service包下的impl包下的任意类的任意方法

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

返回值类型:*

包名类名:com.xyz.service..*

方法名:*

参数:..

service包下的任意路径(包括本路径和子路径)的任意方法

​编辑

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

这个没有类名,本意是想找service包下的所有类的所有方法,但会报错

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

service路径包括子路径的所有类的所有方法,不会报错。注意本例中是..  而上例是.

切入点表达式的用法

(3)使用AspectJ的环境

依赖

       //spring依赖 
       <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.19</version>
        </dependency>
        //aspectJ依赖
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.19</version>
        </dependency>

将配置文件编译时加载到target文件中,不然会显示找不到配置文件

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                </includes>
            </resource>
        </resources>
    </build>

Maven当中resources标签的用法_怪咖软妹@的博客-CSDN博客_maven resources标签

 

4.前置通知

(1)前置通知流程分析

前置通知在目标方法前执行,所以在前置方法中无法获取目标方法执行后的结果,但能获取目标方法的签名,也就是public后面的一堆,因为只有知道目标方法的签名,才能作为他的前置通知。

​编辑

(2)前置通知切面方法开发

 前置通知切面方法开发

​编辑

Step1:创建业务接口

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

Step2:创建业务实现

@Component
public class SomeServiceImpl implements SomeService{
    @Override
    public String doSome(String name, int age) {
        System.out.println("doSome功能实现");
        return "doSome";
    }
}

Step3:创建切面类,实现切面方法

//切面类
@Component
@Aspect //交给AspectJ框架去识别的切面类
public class MyAspect {
    /*
    前置通知的规范
    1.访问权限是public
    2.方法返回值是void
    3.方法名称自定义
    4.方法没有参数,如果有也只能是JoinPoint类型
    5.必须使用@Before注解来声明切入的时机是前切功能和切入点
        参数:value 指定切入点表达式
        public String doSome(String name, int age)
    */
    @Before(value = "execution(public String Before.SomeServiceImpl.doSome(String,int))")
    public void myBefore(){
        System.out.println("前置通知功能实现");
    }
}

Step4:在applicationContext.xml文件中进行切面绑定

<!--创建业务对象-->
    <bean id="someService" class="Before.SomeServiceImpl"></bean>
<!--创建切面对象-->
    <bean id="myAspect" class="Before.MyAspect"></bean>

<!--绑定,之前是通过代理来绑定的,等同于ProxyFactory中的代码-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

或用包来扫描类

    <context:component-scan base-package="Before"></context:component-scan>
<!--绑定,之前是通过代理来绑定的,等同于ProxyFactory中的代码-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

Step:测试

public class Test {
    @org.junit.Test
    public void test(){
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        //取出代理对象
        SomeService someService = (SomeService) ac.getBean("someServiceImpl");
        System.out.println(someService.getClass());//class jdk.proxy2.$Proxy10
        //表示这个someService是加入了切面功能的对象,而不是普通的someService对象
        someService.doSome("1",1);
    }
}

​编辑

JDK动态代理和CGLib动态代理

JDK动态代理只能用接口接收代理对象,而CGLib可以用接口实现类接收代理对象也就是用子类来接收代理对象,可以使用标签来灵活改变代理方式

​编辑

CGLib代理

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

SomeServiceImpl someService = (SomeServiceImpl) ac.getBean("someService");

JDK代理

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

SomeService someService = (SomeService) ac.getBean("someService");

方法参数JoinPoint解析

JoinPoint来获取目标参数的信息

@Component
@Aspect //交给AspectJ框架去识别的切面类
public class MyAspect {
    @Before(value = "execution(public String Before.SomeServiceImpl.doSome(String,int))")
    public void myBefore(JoinPoint joinPoint){
        System.out.println("前置通知功能实现");
        System.out.println("目标方法的签名"+joinPoint.getSignature());
        System.out.println("目标方法的参数"+ Arrays.toString(joinPoint.getArgs()));
    }
}

5.后置通知

(1)后置通知流程分析

​编辑

 后置通知可以修改目标方法的返回值,但也是要分两种情况。

如果目标方法的返回值类型是8种基本类型或String类型,则不可改变,如果目标方法的返回值是引用类型则可以改变。可变与不可变是在测试代码中体现的。

(2)后置通知切面方法开发

/*
    后置通知的规范
    1.访问权限是public
    2.方法返回值是void
    3.方法名称自定义
    4.如果目标方法有返回值,需要写参数,没有返回值则不需要写参数。写参数也可以处理没有返回值的情况,所以一般要写参数
    5.必须使用@AfterReturning注解来声明切入的时机是后切功能和切入点
        参数:value 指定切入点表达式
            returning:指定目标方法的返回值的名称,此名称必须与切面方法的名称一致。
        public String doSome(String name, int age)
    */
    @AfterReturning(value = "execution(* After.*.*(..))",
                    returning = "obj")
    public void myAfterReturning(Object obj){
        System.out.println("后置通知功能实现");
        if(obj != null){
            if(obj instanceof String){//判断是否是String类型
                obj = obj.toString().toUpperCase();
            }
        }
        System.out.println(obj);
    }

判断目标方法的返回值是否是String类型,如果是则转为大写。在后置切面方法中输出的obj为大写,但是在测试类中,测试目标方法的返回值还是小写。

6.环绕通知

拦截目标方法,在目标方法前后增强功能的通知。是功能最强大的通知,一般事务使用此通知。

(1)环绕通知执行流程分析

​编辑

(2)环绕通知切面方法开发

 /*
    环绕通知的规范
    1.访问权限是public
    2.方法返回值是是目标方法的返回值
    3.方法名称自定义
    4.方法有参数,参数就是目标方法
    5.回避异常
    6.使用@Around注解声明是环绕通知
        参数:value:指定切入点表达式
        public String doSome(String name, int age)
    */
    @Around(value = "execution(* Around.*.*(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
        //前切功能实现
        System.out.println("环绕通知前切功能实现");
        //目标方法调用
        Object obj = pjp.proceed(pjp.getArgs());
        //后切功能实现
        System.out.println("环绕通知后切功能实现");
        return obj.toString().toUpperCase();
    }

环绕通知可以随意修改目标方法的返回值。

7.最终通知

无论目标方法是否正常执行,最终通知的代码都会被执行。相当于try-catch-finally中的finally。

    /*
    最终通知的规范
    1.访问权限是public
    2.方法没有返回值
    3.方法名称自定义
    4.方法不需要参数,若需要写则只能写JoinPoint
    5.使用@After注解声明是最终通知
        参数:value:指定切入点表达式
        public String doSome(String name, int age)
    */
    @After(value = "execution(* fin.*.*(..))")
    public void myAfter(){
        System.out.println("最终通知功能实现");
    }

8.为一个方法添加多种通知

可以为一个方法绑定若干通知,而已统一通知类型也可以绑定多个

//切面类
@Component
@Aspect //交给AspectJ框架去识别的切面类
public class MyAspect {
    @Before(value = "execution(*  *(..))")
    public void myBefore(JoinPoint joinPoint){
        System.out.println("前置通知功能实现");
        System.out.println("目标方法的签名"+joinPoint.getSignature());
        System.out.println("目标方法的参数"+ Arrays.toString(joinPoint.getArgs()));
    }

    @AfterReturning(value = "execution(* fin.*.*(..))",
                    returning = "obj")
    public void myAfterReturning(Object obj){
        System.out.println("后置通知功能实现");
        if(obj != null){
            if(obj instanceof String){//判断是否是String类型
                obj = obj.toString().toUpperCase();
            }
        }
    }

    @Around(value = "execution(* fin.*.*(..))")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
        //前切功能实现
        System.out.println("环绕通知前切功能实现");
        //目标方法调用
        Object obj = pjp.proceed(pjp.getArgs());
        //后切功能实现
        System.out.println("环绕通知后切功能实现");
        return obj.toString().toUpperCase();
    }

    @After(value = "execution(* fin.*.*(..))")
    public void myAfter(){
        System.out.println("最终通知功能实现");
    }
}

​编辑

 可以参见出各个通知的执行顺序

环绕通知前切

前置通知

后置通知

最终通知

环绕通知后切

如果多个切面切入同一个切入点,可给切入点起别名简化开发

使用@Pointcut注解,创建空方法,此方法的名称就是别名。

    @After(value = "myCut()")
    public void myAfter(){
        System.out.println("最终通知功能实现");
    }

    @Pointcut(value = "execution(* fin.*.*(..))")
    public void myCut(){

    }



\