手写Spring(2):实现AOP与JdbcTemplate

实现AOP

相关知识

拦截器的invoke方法

Object invoke(Object proxy, Method method, Object[] args) throws Throwable

参数说明

  • Object proxy代理对象本身(动态生成的子类实例)

  • Method method被调用的方法 (如 hello(), morning()

  • Object[] args方法参数(调用时传入的参数)

思路:通过注解的方式实现AOP,客户端自定义一个拦截器,例如Around拦截器,处理增强方法的逻辑,定义一个ProxyResolver传入原始Bean和自定义的拦截器进行代理类的创建;然后实现完整的AOP,定义一个AroundProxyBeanPostProcessor类实现BeanPostProcessor接口,在其中识别指定的注解并创建代理类,最后通过配置类将其放在IOC容器当中

实现ProxyResolver

在IoC容器中,实现动态代理需要用户提供两个Bean:

  1. 原始Bean,即需要被代理的Bean;
  2. 拦截器,即拦截了目标Bean的方法后,会自动调用拦截器实现代理功能。

拦截器需要定义接口,这里我们直接用Java标准库的InvocationHandler,免去了自定义接口。

假定我们已经从IoC容器中获取了原始Bean与实现了InvocationHandler的拦截器Bean,那么就可以编写一个ProxyResolver来实现AOP代理。

ByteBuddy的官网上搜索很容易找到相关代码,我们整理为createProxy()方法:

typescript 复制代码
public class ProxyResolver {
    // ByteBuddy实例:
    ByteBuddy byteBuddy = new ByteBuddy();

    // 传入原始Bean、拦截器,返回代理后的实例:
    public <T> T createProxy(T bean, InvocationHandler handler) {
        // 目标Bean的Class类型:
        Class<?> targetClass = bean.getClass();
        // 动态创建Proxy的Class:
        Class<?> proxyClass = this.byteBuddy
                // 子类用默认无参数构造方法:
                .subclass(targetClass, ConstructorStrategy.Default.DEFAULT_CONSTRUCTOR)
                // 拦截所有public方法:
                .method(ElementMatchers.isPublic()).intercept(InvocationHandlerAdapter.of(
                        // 新的拦截器实例:
                        //这个拦截器的作用是将所有public方法调用转发给外部的handler处理,同时传递原始bean实例、方法信息和参数,实现了AOP的切面功能
                        new InvocationHandler() {
                            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                                // 将方法调用代理至原始Bean:
                                return handler.invoke(bean, method, args);
                            }
                        }))
                // 生成字节码:
                .make()
                // 加载字节码:
                .load(targetClass.getClassLoader()).getLoaded();
        // 创建Proxy实例:
        Object proxy;
        try {
            proxy = proxyClass.getConstructor().newInstance();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return (T) proxy;
    }
}

实现Around

AroundProxyBeanPostProcessor的机制非常简单:检测每个Bean实例是否带有@Around注解,如果有,就根据注解的值查找Bean作为InvocationHandler,最后创建Proxy,返回前保存了原始Bean的引用,因为IoC容器在后续的注入阶段要把相关依赖和值注入到原始Bean。

实现before和after

只需要实现拦截器接口,再实现类中定义before或after方法,后续定义拦截器的时候再实现这个抽象

类就可以了

typescript 复制代码
public abstract class BeforeInvocationHandlerAdapter implements InvocationHandler {

    public abstract void before(Object proxy, Method method, Object[] args);

    @Override
    public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before(proxy, method, args);
        return method.invoke(proxy, args);
    }
}

关键组件说明

1. 核心处理器

  • AroundProxyBeanPostProcessor: AOP入口,处理@Around注解

  • AnnotationProxyBeanPostProcessor: 抽象基类,支持自定义注解

2. 拦截器类型

  • Around: 完全控制方法调用

  • Before : 方法执行前拦截 (BeforeInvocationHandlerAdapter)

  • After : 方法执行后拦截 (AfterInvocationHandlerAdapter)

3. 执行流程特点

初始化阶段:

  • BeanPostProcessor优先创建

  • 检测@Around注解创建代理

  • 属性注入到原始Bean

运行时阶段:

  • 方法调用被代理对象拦截

  • 根据拦截器类型执行相应逻辑

  • 最终调用原始Bean方法

扩展Annotation

假设我们后续编写了一个事务模块,提供注解@Transactional,那么,要启动AOP,就必须仿照AroundProxyBeanPostProcessor,提供一个TransactionProxyBeanPostProcessor,不过复制代码太麻烦了,我们可以改造一下AroundProxyBeanPostProcessor,用泛型代码处理Annotation,先抽象出一个AnnotationProxyBeanPostProcessor

scala 复制代码
public abstract class AnnotationProxyBeanPostProcessor<A extends Annotation> implements BeanPostProcessor {

    Map<String, Object> originBeans = new HashMap<>();
    Class<A> annotationClass;

    public AnnotationProxyBeanPostProcessor() {
        this.annotationClass = getParameterizedType();
    }
    ...
}

实现AroundProxyBeanPostProcessor就一行定义:

java 复制代码
public class AroundProxyBeanPostProcessor extends AnnotationProxyBeanPostProcessor<Around> {
}

后续如果我们想实现@Transactional注解,只需定义:

java 复制代码
public class TransactionalProxyBeanPostProcessor extends AnnotationProxyBeanPostProcessor<Transactional> {
}

就能自动根据@Transactional启动AOP。

实现JdbcTemplate

配置DataSource

使用JdbcTemplate之前,我们需要配置JDBC数据源。Spring本身只提供了基础的DriverManagerDataSource,但Spring Boot有一个默认配置的数据源,并采用HikariCP作为连接池。

实现一个HikariCP支持的DataSource,用配置类 JdbcConfiguration管理

less 复制代码
@Configuration
public class JdbcConfiguration {

    @Bean(destroyMethod = "close")
    DataSource dataSource(
            // properties:
            @Value("${summer.datasource.url}") String url,
            @Value("${summer.datasource.username}") String username,
            @Value("${summer.datasource.password}") String password,
            @Value("${summer.datasource.driver-class-name:}") String driver,
            @Value("${summer.datasource.maximum-pool-size:20}") int maximumPoolSize,
            @Value("${summer.datasource.minimum-pool-size:1}") int minimumPoolSize,
            @Value("${summer.datasource.connection-timeout:30000}") int connTimeout
    ) {
        var config = new HikariConfig();
        config.setAutoCommit(false);
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        if (driver != null) {
            config.setDriverClassName(driver);
        }
        config.setMaximumPoolSize(maximumPoolSize);
        config.setMinimumIdle(minimumPoolSize);
        config.setConnectionTimeout(connTimeout);
        return new HikariDataSource(config);
    }
}

然后我们需要封装一个JdbcTemplate类,注入依赖DataSource,实现对数据库的一些操作 底层实现如下:

scss 复制代码
1. queryForObject(sql, rowMapper, 123)
   ↓
2. execute(psc, pscAction)          // 中间层execute
   ↓  
3. execute(connectionCallback)      // 底层execute
   ↓
4. dataSource.getConnection()       // 获取连接
   ↓
5. conn.setAutoCommit(true)         // 临时启用自动提交
   ↓
6. psc.createPreparedStatement(conn)
   │   ↓
   │   7. conn.prepareStatement("SELECT...")
   │   8. bindArgs(ps, 123) → ps.setObject(1, 123)
   │
   ↓
7. pscAction.doInPreparedStatement(ps)
   │   ↓
   │   10. ps.executeQuery() → ResultSet
   │   11. rs.next() → 遍历结果集
   │   12. rowMapper.mapRow(rs, rowNum) → User对象
   │
   ↓
8. 自动关闭ResultSet (try-with-resources)
9. 自动关闭PreparedStatement (try-with-resources)
10. conn.setAutoCommit(false)       // 恢复原始设置
11. 自动关闭Connection (try-with-resources)

最后把JdbcTemplate类写到配置类即可

实现声明式事务

Spring提供的声明式事务管理能极大地降低应用程序的事务代码。如果使用基于Annotation配置的声明式事务,则一个与数据库操作相关的类只需加上@Transactional注解,就实现了事务支持,非常方便

定义@Transactional注解时,需要指定默认的拦截器名称,默认值platformTransactionManager(接口)表示用名字为platformTransactionManager的Bean来管理事务

我们现在要做的就是实现该接口以及 InvocationHandler接口进行事务拦截器的开发

java 复制代码
public class DataSourceTransactionManager implements
        PlatformTransactionManager, InvocationHandler
{
    static final ThreadLocal<TransactionStatus> transactionStatus = new ThreadLocal<>();
    final DataSource dataSource;

    public DataSourceTransactionManager(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

在invoke函数内部编写事务开启,事务进行以及事务关闭的逻辑

java 复制代码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    TransactionStatus ts = transactionStatus.get();
    if (ts == null) {
        // 当前无事务,开启新事务:
        try (Connection connection = dataSource.getConnection()) {
            final boolean autoCommit = connection.getAutoCommit();
            if (autoCommit) {
                connection.setAutoCommit(false);
            }
            try {
                // 设置ThreadLocal状态:
                transactionStatus.set(new TransactionStatus(connection));
                // 调用业务方法:
                Object r = method.invoke(proxy, args);
                // 提交事务:
                connection.commit();
                // 方法返回:
                return r;
            } catch (InvocationTargetException e) {
                // 回滚事务:
                TransactionException te = new TransactionException(e.getCause());
                try {
                    connection.rollback();
                } catch (SQLException sqle) {
                    te.addSuppressed(sqle);
                }
                throw te;
            } finally {
                // 删除ThreadLocal状态:
                transactionStatus.remove();
                if (autoCommit) {
                    connection.setAutoCommit(true);
                }
            }
        }
    } else {
        // 当前已有事务,加入当前事务执行:
        return method.invoke(proxy, args);
    }
}

但是开启事务以后,如何让其他在当前事务的方法加入进来呢?

我们需要编写一个有关事务的工具类,获取当前事务的连接

csharp 复制代码
public class TransactionalUtils {
    @Nullable
    public static Connection getCurrentConnection() {
        TransactionStatus ts = DataSourceTransactionManager.transactionStatus.get();
        return ts == null ? null : ts.connection;
    }
}

改造当前Template加入事务的逻辑

java 复制代码
public class JdbcTemplate {
    public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
        // 尝试获取当前事务连接:
        Connection current = TransactionalUtils.getCurrentConnection();
        if (current != null) {
            try {
                return action.doInConnection(current);
            } catch (SQLException e) {
                throw new DataAccessException(e);
            }
        }
        // 无事务,从DataSource获取新连接:
        try (Connection newConn = dataSource.getConnection()) {
            return action.doInConnection(newConn);
        } catch (SQLException e) {
            throw new DataAccessException(e);
        }
    }
    ...
}

最后把拦截器, TransactionalBeanPostProcessor,Template加入到配置类中,即可开启AOP管理

相关推荐
rabbit_pro5 小时前
SpringBoot3使用PostGis+GeoTools整合MybatisPlus
java·spring
草履虫建模6 小时前
A13 String 详解:不可变、常量池、equals 与 ==、性能与常见坑
java·开发语言·spring·jdk·intellij-idea·java基础·新手
小马爱打代码7 小时前
Spring AI 实战:Agent 基础搭建与核心能力解析
java·人工智能·spring
To Be Clean Coder9 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
what丶k10 小时前
SpringBoot3 配置文件使用全解析:从基础到实战,解锁灵活配置新姿势
java·数据库·spring boot·spring·spring cloud
RwTo10 小时前
【源码】- SpringBoot启动
java·spring boot·spring
那我掉的头发算什么12 小时前
【Spring】Spring Boot 验证码小项目:Hutool 让图形验证码开发变简单
java·服务器·spring boot·后端·spring
小信丶12 小时前
@Activate 注解详解:应用场景与实战示例
java·spring boot·后端·spring·spring cloud·微服务·dubbo
WZTTMoon12 小时前
spring-boot 升级版本引发的灾难
java·spring boot·spring
css趣多多13 小时前
异步组件核心知识点
前端·vue.js·spring