Spring5(3)-面向切面编程

Spring5(3)-面向切面编程,第1张

目录

一.概念

二.手写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依赖 
       
            org.springframework
            spring-context
            5.3.19
        
        //aspectJ依赖
        
            org.springframework
            spring-aspects
            5.3.19
        

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

    
        
            
                src/main/java
                
                    **/*.xml
                    **/*.properties
                
            
            
                src/main/resources
                
                    **/*.xml
                    **/*.properties
                
            
        
    

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文件中进行切面绑定


    

    


    

或用包来扫描类

    

    

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代理

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

JDK代理

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(){

    }



 

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/906280.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-15
下一篇 2022-05-15

发表评论

登录后才能评论

评论列表(0条)

保存