Spring IoC和AOP--基于SpringBoot AOP开发

Spring

Spring简介

Spring是一个轻量级的框架,是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。Spring以IoC(Inverse Of Control)和AOP(Aspect Oriented Programming)为内核,提供了展现层Spring MVC和持久层Spring JDBC以及业务事务管理等众多的企业级应用技术,还可整合开源众多第三方框架和类库,逐渐成为使用最多的Java EE企业应用开源框架。

Spring主要包含以下七个模块:

  1. Spring Context:提供框架式的Bean访问方式,以及企业及功能(JNDI、定时任务等)。
  2. Spring Core:核心类库,所有功能都依赖于该类库,提供IOC和DI服务。
  3. Spring AOP:AOP服务。
  4. Spring Web:提供了基本的面向Web的综合特性,对常用框架的支持,Spring能够管理这些框架,并将资源注入给框架,也能在这些框架的前后插入拦截器。
  5. Spring MVC:提供面向Web应用的Model-View-Controller。
  6. Spring DAO:堆JDBC的抽象封装,简化了数据库访问异常的处理,并能统一管理JDBC事务。
  7. Spring ORM:对现有的ORM框架的支持。

Spring优点

  1. 方便解耦,简化开发:Spring工厂管理所有对象的创建和依赖关系。
  2. AOP编程支持:提供面向切面编程,方便对程序进行安全、权限、日志和事务等操作。
  3. 声明式事务的支持:只需要通过配置就可以完成对事务的管理。
  4. 方便程序测试:堆Junit4支持,可通过注解方便测试。
  5. 可集成各种优秀的框架:提供对各种优秀框架支持,如MyBatis、Hibernate和Quartz等。
  6. 降低JavaEE API使用难度:对开发中一些难用的API(如JDBC、JavaMail和远程调用等)都提供了封装,降低使用难度。

控制反转

IoC(Inversion of Control )控制反转,意思是将原本在程序中代码直接创建对象的控制权,交给Spring容器来实现对象的组装和管理,所谓的控制反转就是对组件对象的控制权从代码本身交给容器。

控制反转的作用

  • 管理对象的创建和依赖关系的维护:对象的创建不是new了就完事了,在对象关系比较复杂时,如果需要程序猿来维护,那是相当头大。
  • 托管类的产生过程:比如要在类的产生过程做一些事情,就由容器(用代理)去完成,而我们并不需要关系这个过程怎么完成的。

依赖注入

控制反转是个很大的概念,可不用不同的方式来实现,这里主要讲依赖注入DI(Dependency Injection)。所谓的依赖注入即组件之间的关系有容器在程序运行过程来决定,也就是由容器动态的将某种依赖关系的目标对象实例注入到应用系统的各个关联组件之中,组件只提供Java方法让容器决定依赖关系。

Spring框架使用这种方式来实现注入,也是时下最流行的IoC实现方式,依赖注入的方式有三种:

  • 接口注入(Interface Injection)
  • Setter方法注入(Setter Injection)
  • 构造器注入(Constructor Injection)

由于接口注入的灵活性和易用性较差,在Spring4已经被废弃。

AOP

AOP(Aspect Oriented Programming),面向切面编程,用一句话来总结:在程序运行期间,在不改动源代码的情况下,动态的切入指定方法的在指定位置进行运行的编程方式。如何不改动源代码去做这些操作,一般用代理模式实现。

代理模式

什么是代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲类似于我们生活中常见的中介。

作用

中介隔离:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类可以在中间起到作用,其特征是代理类和委托类实现了相同的接口。

开闭原则,增强功能:代理类可以增加额外功能来增强委托类,这样不需要修改委托类,符合代码涉及的开闭原则。代理类本身并不是真正实现服务,而是通过调用委托类的相关方法来提供特定服务,真正的功能还是有委托类实现。

静态代理

这里用明星和经纪人做一个例子,为了方便写在一个文件中

public class TestProxy {
    public static void main(String[] args) {
        Agent agent = new Agent(new Star());
        agent.communicate();
    }
}

interface Action{
    public void communicate();
}

class Star implements Action{ //明星
    @Override
    public void communicate() {
        System.out.println("沟通");
    }
}

class Agent implements Action{ //经纪人
    private Star star;

    public Agent(final Star star) {
        this.star = star;
    }

    @Override
    public void communicate() {
        System.out.println("我是经纪人,沟通前");
        star.communicate();
        System.out.println("沟通后");
    }
}

测试结果:

我是经纪人,沟通前
沟通
沟通后

在程序运行前就已经存在代理的字节码文件,代理对象和真实对象的关系在运行前就确定了。

静态代理总结:

优点:在符合开闭原则的情况下对目标对象功能扩展。

缺点:我们得为每一个对象都创建一个代理类,工作量大,不易管理。而且接口发生变化,代理类和委托类都要修改。

动态代理

在动态代理中我们不需要手动创建代理类,只需要编写一个动态处理器即可。真正的代理对象有JDK在运行时通过反射来动态创建。

ProxyHandler.java

public class ProxyHandler implements InvocationHandler {
    private Object object;

    public ProxyHandler(final Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName() + "执行前");
        Object result = method.invoke(object, args);
        System.out.println(method.getName() + "执行后");
        return result;
    }
}

上面代码我们实现了InvocationHandler接口的invoke()方法,实现我们自己的动态处理器,这里有三个参数:

  • Object proxy:代理对象,给JDK使用,任何时候都不要动这个对象。
  • Method method:当前将要执行的目标方法,method.invoke()方法可利用反射执行目标方法。
  • Object[] args:目标方法传入的参数

写一下测试类,TestDynamicProxy.java

public class TestDynamicProxy {
    public static void main(String[] args) {
        Action action = new Star();
        Action proxyAction = (Action) Proxy.newProxyInstance(Action.class.getClassLoader(),
                action.getClass().getInterfaces(), new ProxyHandler(action));
        proxyAction.communicate();
    }
}

这里调用Proxy.newProxyInstance(),这个方法有三个参数:

  • ClassLoader loader:被代理对象的类加载器

  • Class<?>[] interfaces:被代理对象的接口列表

  • InvocationHandler h:方法执行器,在这里增强代理方法

执行结果:

communicate执行前
沟通
communicate执行后

相对于静态代理,动态代理帮我们大大减少了代码量,同时减少了对业务接口的依赖,降低了耦合度。看到这里你会发现我们AOP要的效果就是这样,动态代理帮我们实现了在程序运行期间,在不改动源代码的情况下,动态的切入指定方法的在指定位置进行运行的编程方式。但是心细的同学会发现动态代理的缺点:

  • 动态代理已经帮我们减少代码量了,但是要给大量的类中的方法创建代理处理器也还是很麻烦。
  • Proxy.newProxyInstance()的一个参数是被代理对象的列表,JDK的默认动态代理,如果目标对象没有实现接口,是无法为它创建代理对象。

Spring AOP底层就是动态代理,可以利用Spring去创建动态代理,几乎不用写代码就可以实现,而且没有强制要求目标对象必须实现接口,如果没有实现接口,则会用CGLIB动态代理实现,CGLIB是采用动态创建子类的方式,所以对于final修饰的方法无法进行代理。

简单的理解其实AOP只做了三件事:

  1. 在哪里切入:例如日志记录,我们要在哪些方法执行的时候记录。
  2. 在什么时候切:是在执行代码之前还是执行之后。
  3. 切入之后做什么事,比如权限校验、日志记录等。

在这里插入图片描述

注解使用AOP

在使用之前先要了解AOP的一些专业术语:

术语名称
Pointcut(切点)在何处切入,切点分为execution和annotation两种方式
advice(处理)包括处理时机和处理内容。处理时机就是在什么时候执行,分为前置处理、后置处理和异常处理等;处理内容就是需要增强的方法
Aspect(切面)即Pointcut和Advice
Joint point(连接点)程序执行的一个点,一个连接点总是代表一个方法执行
Weaving(织入)通过动态代理,在目标对象方法中执行处理内容的过程

这里我们用SpringBoot Web工程来写个例子

引入AOP的依赖,为了方便测试我们也引入test启动器:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springBootTest</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.7.RELEASE</version>
    </parent>
    <dependencies>
        <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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>
</project>

编写启动类:Applecation.java

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

编写TestController.java

@Controller
public class TestController {
    public int add (int a, int b){
        int result = a + b;
        System.out.println("加法方法,计算结果是:"+result);
        return result;
    }
}

编写一个切面配置类:MyAspect.java

@Aspect
@Component
public class MyAspect {

    @Before("execution(public int com.aspect.controller.TestController.add(int,int))")
    public void myBefore(JoinPoint joinPoint){
        System.out.println("执行方法名:"+joinPoint.getSignature().getName()+",【Before】执行前,参数:"+ Arrays.asList(joinPoint.getArgs()));
    }
}

编写测试类:TestAspect.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class TestAspect {

    @Autowired
    TestController testController;
    @Test
    public void test01(){
        testController.add(1, 2);
    }
}

工程结构如下
在这里插入图片描述

执行测试方法,输出:

执行方法名:add,【Before】执行前,参数:[1, 2]
加法方法,计算结果是:3

这里重点讲一下配置类,首先是@Aspect注解声明该类是切面类,@Before标记该方法为前置通知,在目标方法(切入点)前执行,execution切入点表达式匹配连接点,然后得到的效果就是,业务代码没有任何污染的情况下实现了增强。

当然我们不仅可以在方法执行前(前置通知),通知类还有以下几种:

通知类型注解说明
前置通知@Before在目标方法(切入点)执行前执行,value可绑定切点表达式
后置通知@After在目标方法(切入点)执行后执行
返回通知@AfterReturning返回结果自后执行,在@After之后执行
异常通知@AfterThrowing在目标方法抛出异常之后执行,目标方法执行异常不会执行后只通知和返回通知
环绕通知@Around最强大的通知,包含以上全部通知

execution切入点表达式

语法:execution([方法修饰符] 返回类型 方法全类名(参数类型) [异常类型])

用法:

  • 方法修饰符和异常类型可以省略
  • *可以匹配任意一个,…可以匹配任意多个
  • 返回类型可以用*匹配任意类型
  • 可以使用&&、||、!

常用举例,这里只写execution参数

表达式匹配规则
public int com.aspect.controller.TestController.add(int,int)匹配修饰符为public,返回值为int,在com.aspect.controller.TestController类中的add()方法,且入参为两个整数
* com.aspect.controller.TestController.add(*)匹配com.aspect.controller.TestController类中的add()方法,参数只能有一个
* com.aspect.controller.TestController.add(…)匹配com.aspect.controller.TestController类中的add()方法,参数个数不限
* com.aspect.controller.TestController.*(…)匹配com.aspect.controller.TestController类中的任意方法,参数个数不限
* com.aspect.controller.*.*(…)匹配com.aspect.controller包下任意类的任意方法(不含子包),且参数个数不限
* com.aspect.controller…*.*(…)匹配com.aspect.controller包下任意类的任意方法(含子包),且参数个数不限
* com.aspect…*Controller.*(…)匹配com.aspect包及其子孙包下以Controller结尾的类中的任一方法,且参数个数不限
* *(…)最模糊写法,见方法就切,虽然符合语法但是会报错,因为被final修饰的方法是无法进行代理的

@Pointcut把切入点表达式提取出来放在一个空方法上,其他通知注解value即可使用被@Pointcut标记的方法名。

所有的通知都加上,方法正常运行的情况:

@Aspect
@Component
public class MyAspect {
    @Pointcut(value = "execution(* com.aspect.controller.TestController.*(..))")
    public  void exec(){}
    
    @Before("exec()")
    public void myBefore(JoinPoint joinPoint){
        System.out.println("执行方法名:"+joinPoint.getSignature().getName()+",前置通知,参数:"+ Arrays.asList(joinPoint.getArgs()));
    }
    @After("exec()")
    public void after(JoinPoint joinPoint){
        System.out.println("执行方法名:"+joinPoint.getSignature().getName()+",后置通知,参数:"+ Arrays.asList(joinPoint.getArgs()));
    }
    @AfterReturning("exec()")
    public void afterReturning(JoinPoint joinPoint){
        System.out.println("执行方法名:"+joinPoint.getSignature().getName()+",返回通知,参数:"+ Arrays.asList(joinPoint.getArgs()));
    }

    @AfterThrowing("exec()")
    public void afterThrowing(JoinPoint joinPoint){
        System.out.println("执行方法名:"+joinPoint.getSignature().getName()+",异常通知,参数:"+ Arrays.asList(joinPoint.getArgs()));
    }
    @Around("exec()")
    public Object myAround(ProceedingJoinPoint pj){
        Object object = null;
        try {
            System.out.println("执行方法名:"+pj.getSignature().getName()+",环绕前置通知,参数:"+ Arrays.asList(pj.getArgs()));
            object =  pj.proceed(pj.getArgs());
            System.out.println("执行方法名:"+pj.getSignature().getName()+",环绕返回通知,参数:"+ Arrays.asList(pj.getArgs()));
        } catch (Throwable throwable) {
            System.out.println("执行方法名:"+pj.getSignature().getName()+",环绕异常通知,参数:"+ Arrays.asList(pj.getArgs()));
        }finally {
            System.out.println("执行方法名:"+pj.getSignature().getName()+",环绕后置通知,参数:"+ Arrays.asList(pj.getArgs()));
        }
        return object;
    }
}

运行输出:

执行方法名:add,环绕前置通知,参数:[1, 2]
执行方法名:add,前置通知,参数:[1, 2]
加法方法,计算结果是:3
执行方法名:add,环绕返回通知,参数:[1, 2]
执行方法名:add,环绕后置通知,参数:[1, 2]
执行方法名:add,后置通知,参数:[1, 2]
执行方法名:add,返回通知,参数:[1, 2]

方法异常运行的情况:

在TestController.java中增加一个除法计算方法

public int div (int a, int b){
    int result = a / b;
    System.out.println("除法方法,计算结果是:"+result);
    return result;
}

测试类TestAspect.java加一个调用方法:

@Test
public void test02(){
    testController.div(1, 0);
}

运行输出并抛出异常:

执行方法名:div,环绕前置通知,参数:[1, 0]
执行方法名:div,前置通知,参数:[1, 0]
执行方法名:div,环绕异常通知,参数:[1, 0]
执行方法名:div,环绕后置通知,参数:[1, 0]
执行方法名:div,后置通知,参数:[1, 0]
执行方法名:div,返回通知,参数:[1, 0]

但是我们发现异常通知方法没执行,返回通知方法执行了。

总结

没有环绕通知的时候通知方法执行顺序:

  • 方法正常执行:前置通知->目标方法执行->后置通知->返回通知
  • 方法异常执行:前置通知->后置通知->异常通知

有环绕通知的时候通知方法执行顺序:

  • 方法正常执行:环绕前置通知->前置通知->目标方法执行->环绕返回通知->环绕后置通知->后置通知->返回通知

  • 方法异常执行:

    • 环绕捕获异常:环绕前置通知->前置通知->环绕异常通知->环绕后置通知->后置通知->返回通知(环绕捕获了异常所以外面的通知感知不到异常信息,就正常执行了)

    • 环绕抛出异常:环绕前置通知->前置通知->环绕异常通知->环绕后置通知->后置通知->异常通知

    @Around("exec()")
    public Object myAround(ProceedingJoinPoint pj){
        Object object = null;
        try {
            System.out.println("执行方法名:"+pj.getSignature().getName()+",环绕前置通知,参数:"+ Arrays.asList(pj.getArgs()));
            object =  pj.proceed(pj.getArgs());
            System.out.println("执行方法名:"+pj.getSignature().getName()+",环绕返回通知,参数:"+ Arrays.asList(pj.getArgs()));
        } catch (Throwable throwable) {
            System.out.println("执行方法名:"+pj.getSignature().getName()+",环绕异常通知,参数:"+ Arrays.asList(pj.getArgs()));
            throw new RuntimeException(throwable); //抛出异常
        }finally {
            System.out.println("执行方法名:"+pj.getSignature().getName()+",环绕后置通知,参数:"+ Arrays.asList(pj.getArgs()));
        }
        return object;
    }

Spring事务请看这篇文章:Spring 事务–如何在开发中熟练使用事务

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值