第四章 约定编程-Spring AOP

若有错,请指出
第二章 搭建Springboot环境,配置视图解析器jsp页面
第三章 全注解下的Spring Ioc
第四章 约定编程-Spring AOP
第五章 Spring Boot的数据库编程

4.1 动态代理

将通用的代码抽取出来简化开发流程
在这里插入图片描述
新建项目模拟吃饭流程
在这里插入图片描述
在这里插入图片描述

4.1.1 概念

动态代理的意义在于生成一个占位(又称代理对象)来代理目标对象,从而增强或者控制真实对象的访问。主要包括JDK和CGLIB 这两种常用的

  1. 存在接口的时候默认使用JDK动态代理,否则CGLIB代理
  2. JDK动态代理要求类存在接口,CGLIB不需要
  3. 推荐使用接口的方式编程,定义(接口)与实现(实现类)相分离具有更好的扩展性

在这副图中假设这样一个场景,记者想要采访儿童问题,儿童(目标对象)无法准确回答,那么父母(代理对象)就可以帮助回答。
又比如房客,中介,房东中,房客想要与房东(目标对象)签订合同可通过中介(代理对象)执行签订。
在这里插入图片描述

4.1.2 JDK动态代理实现步骤

1.建立目标对象与代理对象的关联关系
2.实现代理对象的逻辑方法(InvocationHandler接口的invoke方法)
目录结构
在这里插入图片描述

新建接口吃饭接口

public interface DinnerService {
    public void have(String desc, Date date);
}

吃饭实现类(目标对象)

//目标对象
@Service
public class LunchServiceImpl implements DinnerService {
    @Override
    public void have(String desc, Date date) {
        System.out.println("吃午饭,吃的是【"+desc+"】,时间是【"+date+"】");
        //throw new RuntimeException("发生了急事");//模拟发生急事
    }
}

代理类实现动态代理逻辑

//动态代理:代理对象实现InvocationHandler接口的invoke方法
public class DinnerProxy implements InvocationHandler {
    //目标对象,房东
    private Object target=null;

    //1.建立代理对象与目标对象的关联关系(中介和房东签署合同)
    public static Object bind(Object target){
        DinnerProxy dinnerProxy=new DinnerProxy();
        dinnerProxy.target=target;
        //生成代理对象
        Object proxy= Proxy.newProxyInstance(
                //类加载器
                target.getClass().getClassLoader(),
                //绑定的接口,把代理对象绑定到哪些接口下,可以是多个
                target.getClass().getInterfaces(),
                dinnerProxy
        );
        return proxy;
    }

    /**
     *
     * @param proxy 代理对象
     * @param method    方法
     * @param args  参数
     * @return  方法调用结果
     * @throws Throwable 异常
     */
    //2.代理的逻辑方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("洗手");
        //吃饭,代理对象执行目标对象逻辑方法的方法
        Object obj=method.invoke(target,args);
        System.out.println("收拾饭菜");
        System.out.println("收拾餐具");
        return obj;
    }
}

创建主类

//测试动态代理执行类
public class AopMain {
    public static void main(String[] args){
        //新建目标对象
        DinnerService target=new LunchServiceImpl();
        //绑定目标对象,
        DinnerService proxy=(DinnerService) DinnerProxy.bind(target);
        //执行代理对象的invoke方法代替目标对象实现have方法
        proxy.have();
    }
}

为什么代理对象这里能直接强制转换到目标对象的接口呢?

DinnerService proxy=(DinnerService) DinnerProxy.bind(target);

原因是在第一步中,第二个参数中下挂到目标对象的接口
在这里插入图片描述
运行截图
在这里插入图片描述

4.2 约定编程

4.2.1 案例

模拟吃饭发生急事
在这里插入图片描述
在这里插入图片描述
吃饭这个过程就是目标对象的方法,其他过程如洗手之类的过程则抽离出来,实现逻辑过程写入拦截器类DinnerInterceptor实现类中,简化DinnerProxy的invoke方法代码量

目录结构
在这里插入图片描述
新增DinnerInterceptor接口实现类DinnerInterceptorImpl

public class DinnerInterceptorImpl implements DinnerInterceptor {
            @Override
            public void beforeDinner() {
                System.out.println("饭前洗手");
            }

            @Override
            public void afterDinner() {
                System.out.println("收拾餐具");
            }

            @Override
            public void afterThrowing() {
                System.out.println("处理急事");
            }

            @Override
            public void after() {
                System.out.println("处理饭菜");
            }
}

修改DinnerProxy类

//动态代理:代理对象实现InvocationHandler接口的invoke方法
//该类实现将服务类和拦截方法织入对应流程
public class DinnerProxy implements InvocationHandler {
    //目标对象,房东
    private Object target=null;
    //拦截器
    private DinnerInterceptor dinnerInterceptor=null;

    //1.建立代理对象与目标对象的关联关系(中介和房东签署合同)
    public static Object bind(Object target,DinnerInterceptor dinnerInterceptor){
        //1.创建DinnerProxy對象
        DinnerProxy dinnerProxy= new DinnerProxy();
        //2.DinnerProxy對象賦值目標對象值
        dinnerProxy.target=target;
        //3.DinnerProxy對象赋值dinnerInterceptor
        dinnerProxy.dinnerInterceptor=dinnerInterceptor;
        //4.生成代理对象
        Object proxy= Proxy.newProxyInstance(
                //类加载器
                target.getClass().getClassLoader(),
                //绑定的接口,把代理对象绑定到哪些接口下,可以是多个
                target.getClass().getInterfaces(),
                //绑定代理对象实现逻辑
                dinnerProxy
        );
        return proxy;
    }


    /**
     *
     * @param proxy 代理对象
     * @param method    方法
     * @param args  参数
     * @return  方法调用结果
     * @throws Throwable 异常
     */
    //2.代理的逻辑方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //洗手
        this.dinnerInterceptor.beforeDinner();
        //吃饭
        Object obj=null;
        boolean flag=false;//表示是否发生异常
        try{
            obj=method.invoke(target,args);
        }catch (Exception e){
            flag=true;
        }finally {
            //不管有没有急事,吃完饭都要处理饭菜
            this.dinnerInterceptor.after();
        }
        //处理急事
        if(flag){
            this.dinnerInterceptor.afterThrowing();
        }
        //没急事就收拾餐具
        else{
            this.dinnerInterceptor.afterDinner();
        }
        return obj;
}

修改AopMain

//测试动态代理执行类
public class AopMain {
    public static void main(String[] args){
        //新建目标对象
        DinnerService target=new LunchServiceImpl();
        //绑定目标对象
        DinnerService proxy=(DinnerService) DinnerProxy.bind(target,new DinnerInterceptorImpl());
        //执行代理对象的invoke方法
        proxy.have();
    }
}

运行截图
在这里插入图片描述
抛出异常表示发生急事
在这里插入图片描述
在这里插入图片描述

4.2.2 设计者的思维

1.暴露设计接口
2.给出约定的流程图
3.屏蔽实现动态代理的细节(DinnerProxy类),因为这些比较抽象
4.对于开发者只需要知道接口即可
改动一下就可
新建BreakfastServiceImpl类

@Service
public class BreakfastServiceImpl implements DinnerService {
    @Override
    public void have() {
        System.out.println("吃早饭");
        //throw new RuntimeException("发生了急事");//模拟发生急事
    }
}

在这里插入图片描述
在这里插入图片描述
通过这样子就能方便代理对象切换目标对象实现逻辑方法

4.3 AOP

4.3.1 AOP概念

在这里插入图片描述

4.3.2 AOP开发

1.定义切面
通过它可以描述AOP其他的信息,用来描述流程的织入
织入流程图
在这里插入图片描述

2.确定连接点
确定是在什么地方需要AOP,即在Spring中是什么类的什么方法(确定需要拦截什么方法)
目录结构
在这里插入图片描述

引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

定义切面类(MyAspect) 和连接点(BreakfastServiceImpl.have()方法)

//定义切面,对应拦截器的概念
@Aspect
@Component//让切面能被扫描
public class MyAspect {

    /** execution 拦截正则式匹配方法
     *  * 任何返回类型的方法
     *  com.springboot.chapter4.service.impl.BreakfastServiceImpl 目标对象的全限定名称
     *  have 目标对象方法
     *  (..) 任意参数匹配
     */
    //前置通知,正则式:定义什么时候用AOP
    @Before("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
    public void before(){
        System.out.println("前置通知");
    }
    //后置通知
    @After("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
    public void After(){
        System.out.println("后置通知");
    }
    //异常通知
    @AfterThrowing("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
    public void AfterThrowing(){
        System.out.println("异常通知");
    }
    //返回通知
    @AfterReturning("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
    public void AfterReturning(){
        System.out.println("返回通知");
    }

}

我们也可以用定义切点@Pointcut注解简化连接点的编写

    //定义切点,用这个就不用重复写execution了
    @Pointcut("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
    public void pointcut(){

    }
    //前置通知,正则式:定义什么时候用AOP
    @Before("pointcut()")//args(desc,date),获取对象方法参数,只能用于非环绕通知
    public void before(){
        System.out.println("前置通知");
    }
    //后置通知
    @After("pointcut()")
    public void After(){
        System.out.println("后置通知");
    }
    //异常通知
    @AfterThrowing("pointcut()")
    public void AfterThrowing(){
        System.out.println("异常通知");
    }
    //返回通知
    @AfterReturning("pointcut()")
    public void AfterReturning(){
        System.out.println("返回通知");
    }

IOC容器配置类

@EnableAspectJAutoProxy//驱动Spring AOP的注解
@Configuration
@ComponentScan(value = "com.springboot.chapter4.*")
public class AppConfig {
}

测试类

//测试动态代理执行类
public class AopMain {
    public static void main(String[] args){
        AnnotationConfigApplicationContext context=null;
        try {
            //创建IOC容器,根据AppConfig配置类
            context=new AnnotationConfigApplicationContext(AppConfig.class);
            //DinnerService有两个实现类,不能仅仅通过类型获取Bean,这里是名字+类型获取Bean
            DinnerService service=context.getBean("breakfastServiceImpl",DinnerService.class);
            service.have();
        }finally {
            //关闭IOC容器
            context.close();
        }
    }
}

运行截图
在这里插入图片描述
在这里插入图片描述
AOP可以把大量公共的代码(吃饭前奏、饭后收拾、急事处理)提取出来,然后单独实现,开发者就只要是实现原有方法(吃饭),减少开发者的代码量
(实现)减少开发者的代码
在事务AOP织入中
在这里插入图片描述

在这里插入图片描述

4.3.3 环绕通知

@Before在切入点开始处切入内容
@After在切入点结尾处切入内容
@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)
@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑

环绕通知是一个取代原有目标对象方法的通知,也提供了回调原有目标对像方法的能力(尽量不用它,对代码改动大)
环绕通知可以获取一个ProceedingJoinPoint参数,里面的proceed方法可以回调目标对象方法
在这里插入图片描述
在这里插入图片描述
非环绕通知注解标注的方法上可用参数只能为JoinPoint(被Spring封装过的对象),环绕通知为ProceedingJoinPoint(里面有proceed方法用于回调目标对象方法),继承关系如下图
在这里插入图片描述
通知上获取参数流程
修改服务接口和实现类

public interface DinnerService {
    public void have(String desc, Date date);
}

@Service
public class BreakfastServiceImpl implements DinnerService {
    @Override
    public void have(String desc, Date date) {
        System.out.println("吃早饭,吃的是【"+desc+"】,时间是【"+date+"】");
    }
}

@Service
public class LunchServiceImpl implements DinnerService {
    @Override
    public void have(String desc, Date date) {
        System.out.println("吃午饭,吃的是【"+desc+"】,时间是【"+date+"】");
        //throw new RuntimeException("发生了急事");//模拟发生急事
    }
}

修改Aspect

//定义切面,对应拦截器的概念
@Aspect
@Component
public class MyAspect {
    //定义切点,用这个就不用重复写execution了
    @Pointcut("execution(* com.springboot.chapter4.service.impl.BreakfastServiceImpl.have(..))")
    public void pointcut(){

    }
	//前置通知
    @Before("pointcut() && args(desc,date)")//args(desc,date),传递对象方法参数,只能用于非环绕通知
    public void before(JoinPoint joinPoint, String desc, Date date){
        Object []args=joinPoint.getArgs();//获取目标对象方法参数
        System.out.println("前置通知"+"参数为desc:"+desc+" date:"+date);//打印的是对象地址
    }
    //后置通知
    @After("pointcut()")
    public void After(){
        System.out.println("后置通知");
    }
    //异常通知
    @AfterThrowing("pointcut()")
    public void AfterThrowing(){
        System.out.println("异常通知");
    }
    //返回通知
    @AfterReturning("pointcut()")
    public void AfterReturning(){
        System.out.println("返回通知");
    }

    //环绕通知
    @Around("pointcut()")
    public void Around(ProceedingJoinPoint joinPoint)throws Throwable{
        System.out.println("环绕通知前");
        Object[] objects=joinPoint.getArgs();//环绕通知获取方法参数
        joinPoint.proceed();//回调目标对象方法
        System.out.println("环绕通知后,desc:"+objects[0]);
    }
}

修改AopMain

public class AopMain {
    public static void main(String[] args){
        AnnotationConfigApplicationContext context=null;
        try {
            //创建IOC容器,根据AppConfig配置类
            context=new AnnotationConfigApplicationContext(AppConfig.class);
            //DinnerService有两个实现类,不能仅仅通过类型获取Bean,这里是名字+类型获取Bean
            DinnerService service=context.getBean("breakfastServiceImpl",DinnerService.class);
            service.have("包子",new Date());
        }finally {
            //关闭IOC容器
            context.close();
        }
}

DEBUG方式运行并查看参数
在这里插入图片描述

4.3.4 引入

增强bean接口的功能,比如吃饭接口have方法参数desc为空,我就增加一个校验功能判断不能为空
目录结构
在这里插入图片描述
增加检测接口和实现类

//检测接口
public interface BreakFastValidator {
    public boolean validator(String desc);
}

import com.springboot.chapter4.validator.BreakFastValidator;
import org.springframework.util.StringUtils;

public class BreakFastValidatorImpl implements BreakFastValidator {
    @Override
    public boolean validator(String desc) {
        return StringUtils.isEmpty(desc);//为空放回false
    }
}

在MyAspect类添加下面代码

    //声明检测接口
    @DeclareParents(value = "com.springboot.chapter4.service.impl.BreakfastServiceImpl+",
            defaultImpl = BreakFastValidatorImpl.class)
    private BreakFastValidator breakFastValidator;

修改AOPMain方法

    public static void main(String[] args){
        AnnotationConfigApplicationContext context=null;
        try {
            //测试引入增强接口功能
            //创建IOC容器
            context=new AnnotationConfigApplicationContext(AppConfig.class);
            //Spring Bean,context.getBean()就是得到代理对象
            DinnerService dinnerService=context.getBean("breakfastServiceImpl",DinnerService.class);
            //转换为BreakFastValidator接口,调用Spring Bean增强的validator方法
            //代理对象的下挂的接口DinnerService和BreakFastValidator之间可以相互强制转换
            BreakFastValidator breakFastValidator=(BreakFastValidator) dinnerService;
            String desc="";//假设描述为空
            if(breakFastValidator.validator(desc)){
                System.out.println("还没有具体吃什么");
            }else {
                //不为空执行方法
                dinnerService.have("包子",new Date());
            }
        }finally {
            //关闭IOC容器
            context.close();
        }

为什么可以强制转换BreakFastValidator breakFastValidator=(BreakFastValidator) dinnerService;?
因为代理对象可以下挂多个接口,那么代理对象可以在接口之间相互转换,即DinnerService和BreakFastValidator之间可以相互强制转换
在这里插入图片描述
运行截图
在这里插入图片描述

4.3.5 多切面和责任链模式

如果有多个切面,那么他们各个通知顺序是如何的?
建立三个切面(代码一样),之前切面注解注释掉

@Aspect
@Component
public class MyAspect1 {
    @Pointcut("execution(* com.springboot.chapter4.service.Impl.BreakFastServiceImpl.have(..))")
    public void pointcut(){

    }
    @Before("pointcut()")
    public void Before(){
        System.out.println("MyAspect1的Before方法");
    }
    @After("")
    public void After(){
        System.out.println("MyAspect1的After方法");
    }
    @AfterThrowing("")
    public void AfterThrowing(){
        System.out.println("MyAspect1的AfterThrowing方法");
    }
    @AfterReturning("")
    public void AfterReturning(){
        System.out.println("MyAspect1的AfterReturning方法");
    }
}

AOPMain方法

        AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
        try{
            DinnerService dinnerService=context.getBean("breakFastServiceImpl",DinnerService.class);
            String desc="酸菜鱼";
            dinnerService.have(desc,new Date());
        }finally {
            context.close();
        }

运行截图
在这里插入图片描述
两种方法定义切面顺序,数字小的先执行,数字多少无所谓;接口优先级高于注解,但是用注解更方便
1.在切面类添加注解@Order(num)
2.在切面类实现接口Ordered

    public int getOrder() {
        return 0;
    }

4.3.6 责任链模式

在这里插入图片描述
在这里插入图片描述
MyBatis的插件原理就是代理的代理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值