menu ChaYedan
Spring AOP详解
635 浏览 | 2020-05-18 | 阅读时间: 约 6 分钟 | 分类: Java | 标签: Java,spring,框架
请注意,本文编写于 769 天前,最后修改于 769 天前,其中某些信息可能已经过时。

Spring AOP详解

写在最前

讲完IoC,那么下面就开始讲AOP。对于系统中普通的业务关注点,OOP可以很好地对其进行分解并使之模块化,但却无法更好地避免类似于系统需求的实现在系统中各处散落这样的问题。所以,我们要寻求一种更好的方法,它可以在OOP的基础上更上一层楼,提出一套全新的方法论来避免以上问题,也可以提供某种方法对基于OOP的开发模式做-一个补足,帮助OOP以更好的方式解决以上问题。但迄今为止,我们还找不到比OOP更加有效的软件开发模式。不过,好消息是,我们找到了后者,那就是AOP。

AOP的概念

AOP全称为Aspect-Oriented Programming,中文通常翻译为面向方面编程。使用AOP,我们可以对类似于Logging和Security等系统需求进行模块化的组织,简化系统需求与实现之间的对比关系,进而使得整个系统的实现更具模块化。

怎么来理解呢?

下面就拿我们最常见的代码开始说起吧

下图是我们经常会写到的代码结构

我们这时候通常已经降事务的代码封装成一个类了,然后开启事务,提交事务,回滚事务都是调用的封装类的某个方法。但是我们发现,在每一个Service里面,我们几乎都会写上这样的代码。写一个还好,写十个,百个呢?肯定会写到抓狂。而且,对于我们一直提倡的解耦解耦,这种代码明显是破坏了我们的解耦理念的,业务的代码在某种程度上被污染了。

那怎么办?

我们可能第一时间想到的是抽,继续抽。但其实是不行的。因为这些事务逻辑是依附到我们的业务类逻辑方法中的。而且,我们常说的抽取,是纵向抽取,纵向的抽取在这个时候已经到极限了(都只剩一行代码了)。

那我们重新回来想想,现在想的是把事务代码剔除出去,不让它来污染我们的代码,也就是下面这样

然后明确我们的目的,也就是要在业务逻辑含有我们事务的功能,也就是增强我们的业务逻辑或者说装饰我们的业务逻辑但又不影响我们业务本身的代码。说到这个时候,就是不是想起了某个我们熟悉的东西?没错,我们可以使用装饰者模式,JAVA中的动态代理来实现这种功能(也就是来套娃)

所以回到正题,最后我们的这样抽取,也就是横向抽取,就叫做面向切面编程。之所以这么叫,可以看下书中的图

与主要业务逻辑无关的需求,就要切面一样插入到业务中,实现了业务的系统需求。

AOP的术语

豆知识

书上介绍了蛮多,但我还是挑重点记录一下

AOP的实现很多,将AOP的Aspect织入到OOP系统的实现方式可谓千差万别。但不管如何实现,织入过程是处于AOP和OOP的开发过程之外的,而且对于整个系统的实现是透明的,开发者只需要关注相应的业务需求实现,或者系统需求的实现即可。当所有业务需求和系统需求以模块化的形式开发完成之后,通过织入过程就可以将整个的软件系统集成并付诸使用。

AOP的实现也分为静态AOP实现和动态AOP实现

其中Spring是使用的动态AOP实现

Java平台上的AOP实现机制

  • 动态代理
  • 动态字节码增强
  • Java代码生成
  • 自定义类加载器
  • AOL拓展

需要掌握的术语

Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。

Pointcut(切入点):指我们要对哪些 Joinpoint 进行拦截的定义。更通俗的说,就是我们要增强的方法。

Advice(通知/ 增强):指拦截到 Joinpoint 之后所要做的事情就是通知。相当于Class中的Method。通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。

Aspect(切面):是Pointcut和Advice的结合。

Target(目标对象):代理的目标对象。

Weaving(织入):指把增强应用到目标对象来创建新的代理对象的过程。spring 采用动态代理织入

Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类。

Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。SpringAOP没实现。。。

开始使用之前

在使用之前,先翻一翻Spring文档

5.6.1. Spring AOP or Full AspectJ?

Use the simplest thing that can work. Spring AOP is simpler than using full AspectJ, as there is no requirement to introduce the AspectJ compiler / weaver into your development and build processes. If you only need to advise the execution of operations on Spring beans, Spring AOP is the right choice. If you need to advise objects not managed by the Spring container (such as domain objects, typically), you need to use AspectJ. You also need to use AspectJ if you wish to advise join points other than simple method executions (for example, field get or set join points and so on).

When you use AspectJ, you have the choice of the AspectJ language syntax (also known as the “code style”) or the @AspectJ annotation style. Clearly, if you do not use Java 5+, the choice has been made for you: Use the code style. If aspects play a large role in your design, and you are able to use the AspectJ Development Tools (AJDT) plugin for Eclipse, the AspectJ language syntax is the preferred option. It is cleaner and simpler because the language was purposefully designed for writing aspects. If you do not use Eclipse or have only a few aspects that do not play a major role in your application, you may want to consider using the @AspectJ style, sticking with regular Java compilation in your IDE, and adding an aspect weaving phase to your build script.

大意是Spring实现了一些该实现的东西,但是,如果你有其他需求的话,需要引入完整的AspectJ。所以,我还是两个都引入吧。。。aspectj主要是用来解析execution里面的表达式的

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>

使用AOP

XML形式

直接上代码,结合代码说吧

下面是我我单独抽取出来的日志通知对象

package com.chayedan.util;

import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;

/**
 * 日志通知对象
 */
@Component("logAdvisor")
public class LogAdvisor {
    /**
     * 前置通知
     */
    public void before(){
        System.out.println("方法之前运行的共有逻辑");

    }
    /**
     * 后置通知
     */
    public void after(){
        System.out.println("方法之后运行的共有逻辑");

    }

    /**
     * 异常通知
     */
    public void throwing(){
        System.out.println("方法异常运行的共有逻辑");

    }

    /**
     * 最终通知
     */
    public void fin(){
        System.out.println("无论如何最后都要执行运行的共有逻辑");

    }

    /**
     * 环绕通知
     * 可传参joinPoint,也就是对象方法本身
     */
    public Object around(ProceedingJoinPoint joinPoint){
        try {
            System.out.println("方法之前");
            Object o = joinPoint.proceed();
            System.out.println("方法之后 ");
            return o;
        }catch (Throwable e){
            System.out.println("方法异常");
            throw new RuntimeException( e);
        }finally {
            System.out.println("最终执行");
        }
    }

}

下面是我要增强的实现类

@Service
public class AccountServiceImpl implements AccountService {
    @Override
    public void save() {
        System.out.println("保存逻辑执行...");
    }

    @Override
    public void findById(int id) {
        System.out.println("查找逻辑执行...");
    }

    @Override
    public void update() {
        System.out.println("更新逻辑执行...");
    }

    @Override
    public void delById(int id) {
        System.out.println("删除逻辑执行...");
    }
}

那么现在开始使用AOP对实现类进行增加日志的功能

需要在spring的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:context="http://www.springframework.org/schema/context"
       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/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">
    <!--包扫描-->
    <context:component-scan base-package="com.chayedan"/>

    <!--
        配置
    -->
    <aop:config>
        <!--
            advisor 负责加强的类是谁
        -->
        <aop:aspect id="log" ref="logAdvisor">
            <aop:pointcut id="test" expression="execution(* com.chayedan.service..*.*(..))"></aop:pointcut>

            <aop:before method="before" pointcut-ref="test"></aop:before>
            <aop:after method="after" pointcut-ref="test"></aop:after>
            <aop:after-throwing method="throwing" pointcut-ref="test"></aop:after-throwing>
            <aop:after-returning method="fin" pointcut-ref="test"></aop:after-returning>

            <aop:around method="around" pointcut-ref="test"></aop:around>

        </aop:aspect>

    </aop:config>

</beans>

首先是约束文件,xmlns:aop="http://www.springframework.org/schema/aop"用来引入约束文件。

<aop:aspect id="log" ref="logAdvisor">这代表的就是一个切面,id自己取。ref属性跟IoC里面的一样,指你要使用那个增强类。这里写logAdvisor的原因是因为@Component("logAdvisor"),即那个LogAdvisor类注册到容器里的id是logAdvisor。

里面有两种写法,一个是像我上面那样,把切入点提取出来,下面直接引用,即

<aop:pointcut id="test" expression="execution( * com.chayedan.service..*.*(..))"></aop:pointcut>
<aop:before method="before" pointcut-ref="test"></aop:before>

另一种就是直接写在里面,比如

<aop:before method="before" pointcut-ref="execution( * com.chayedan.service..*.*(..))"></aop:before>

解释一下*的含义

* com.chayedan.service..*.*(..):表示返回值是任意类型的在com.chayedan.service包及其子包下的所有类的所有参数为任意的方法

如果你想增强某些特定的方法,例如

* com.chayedan.service..*.*save(String,int):表示返回值是任意类型的在com.chayedan.service包及其子包下的方法名为save结尾的,方法参数为string和int类型的方法

aop:before 表示在切入点之前执行

after-returning:表示在切入点之后执行

aop:after-throwing:表示在切入点抛出异常是执行

aop:after: 类似于finally,切入点执行完以后无论如果都会执行的方法。

aop:around:最为常用的方法。它是 spring 框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。ProceedingJoinPoint类代表我们的方法,它有一个方法 proceed()。调用即执行方法。返回值封装为Object。

注解形式

还是利用代码来讲吧

@Configuration
@ComponentScan("com.chayedan")
@PropertySource("classpath:db.properties")
/*
* 启用切面代理注解
* */
@EnableAspectJAutoProxy
public class Config {
    @Value("${jdbc.driver}")
    private String driverClassName;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;


    @Bean
    public DataSource getDataSource(){
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setDriverClassName(driverClassName);
        hikariDataSource.setJdbcUrl(url);
        hikariDataSource.setUsername(username);
        hikariDataSource.setPassword(password);
        return hikariDataSource;
    }

    @Bean
    public QueryRunner getQr(DataSource dataSource){
        return new QueryRunner(dataSource);
    }

}

最重要的一点就是在配置类中开启切面代理@EnableAspectJAutoProxy

然后在表明负责加强的类是哪个类,利用@Aspect注解

/**
 * 事务管理对象
 */
@Component("transactionManager")
@Aspect
public class TransactionManager {
    @Autowired
    private ConnectionHolder connectionHolder;

    public void begin() throws SQLException {
        connectionHolder.getConnection().setAutoCommit(false);
    }

    public void commit() throws SQLException {
        connectionHolder.getConnection().commit();
    }

    public void rollback(){
        try {
            connectionHolder.getConnection().rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void close(){
        try {
            Connection connection = connectionHolder.getConnection();
            if (connection!=null){
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    @Around("execution(* com.chayedan.service..*.*(..))") // 环绕通知
    public Object around(ProceedingJoinPoint joinPoint){
        try {
            begin();
            Object o = joinPoint.proceed();

            commit();
            return o;
        }catch (Throwable e){
            rollback();
            throw new RuntimeException(e);
        }finally {
            close();
        }
    }
}

@Aspect注解就相当于aop:aspect

上面演示了环绕通知的注解,直接表明在方法上。对应XML中的<aop:around method="around" pointcut-ref="test"></aop:around>

还有其他几个注解

@Before

@AfterReturning

@AfterThrowing

@After

使用方法同上

备注

如果需要使用半XML半注解。即没有Config类

记得在XML文件中开启切面代理

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,快来留言吧!