【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战(中)

简介: 【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战(中)

【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战(上):https://developer.aliyun.com/article/1390148

config目录各文件介绍

定义Spring AOP的切面类 DataSourceAop

💧DataSourceAop 是一个Spring AOP切面类,用于拦截方法调用,并根据方法的特定条件来选择数据源类型。它通过@Pointcut定义了两个切点表达式,分别用于读操作和写操作的方法。在前置通知方法中,根据目标方法上是否存在 @Master 注解,来决定使用主库还是从库。这样,通过AOP的切面功能,实现了数据库的读写分离。

package com.lxr.demo.config;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
 * 默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)
 * <p>
 * 切面不能建立在DAO层,事务是在service开启的,到dao层再切换数据源,那事务就废了
 */
@Aspect
@Component
public class DataSourceAop {
    /**
     * 第一个”*“符号 表示返回值的类型任意;
     * com.sample.service.impl  AOP所切的服务的包名,即,我们的业务部分
     * 包名后面的”..“  表示当前包及子包
     * 第二个”*“ 表示类名,*即所有类。此处可以自定义,下文有举例
     * .*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型
     * <p>
     * 这是一个切点表达式,它定义了一个切点,该切点在执行以下条件时成立:
     * !@annotation(com.lxr.demo.config.Master): 这表示切点会排除那些带有@com.lxr.demo.config.Master注解的方法。
     * execution(* com.lxr.demo.service.*.select*(..)):
     * 表示切点会包含所有com.lxr.demo.service包下以select开头的方法,并且方法参数可以是任意个数、任意类型。
     * execution(* com.lxr.demo.service..*.find*(..)):
     * 表示切点会包含所有com.lxr.demo.service包及其子包下以find开头的方法,并且方法参数可以是任意个数、任意类型。
     */
    @Pointcut("!@annotation(com.lxr.demo.config.Master) " +
            "&& (execution(* com.lxr.demo.service.*.select*(..)) || execution(* com.lxr.demo.service..*.find*(..))  ) ")
    public void readPointcut() {
    }
    @Pointcut("@annotation(com.lxr.demo.config.Master) " +
            "|| execution(* com.lxr.demo.service..*.save*(..)) " +
            "|| execution(* com.lxr.demo.service..*.add*(..)) " +
            "|| execution(* com.lxr.demo.service..*.insert*(..)) " +
            "|| execution(* com.lxr.demo.service..*.update*(..)) " +
            "|| execution(* com.lxr.demo.service..*.edit*(..)) " +
            "|| execution(* com.lxr.demo..*.delete*(..)) " +
            "|| execution(* com.lxr.demo..*.remove*(..))")
    public void writePointcut() {
    }
    @Before("readPointcut()")
    public void read(JoinPoint jp) {
/**
 * JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.
 * 常用api:
 *
 * 方法名  功能
 * Signature getSignature();  获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
 * Object[] getArgs();  获取传入目标方法的参数对象
 * Object getTarget();  获取被代理的对象
 * Object getThis();  获取代理对象
 */
        //获取当前的方法信息
        MethodSignature methodSignature = (MethodSignature) jp.getSignature();//方法头指定修饰符(例如static)、返回值类型、方法名、和形式参数。
        Method method = methodSignature.getMethod();
        //判断方法上是否存在注解@Master
        boolean present = method.isAnnotationPresent(Master.class);//判断注解是否存在该元素上,如果有则返回true,否则false
        if (!present) {
            //如果不存在,默认走从库读
            System.out.println("no");
            DBContextHolder.slave();
        } else {
            //如果存在,走主库读
            System.out.println("yes");
            DBContextHolder.master();
        }
    }
    @Before("writePointcut()")
    public void write() {
        System.out.println("write");
        DBContextHolder.master();
    }
    /**
     * 另一种写法:if...else...  判断哪些需要读从数据库,其余的走主数据库
     */
//    @Before("execution(* com.cjs.example.service.impl.*.*(..))")
//    public void before(JoinPoint jp) {
//        String methodName = jp.getSignature().getName();
//
//        if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
//            DBContextHolder.slave();
//        }else {
//            DBContextHolder.master();
//        }
//    }
}

配置数据源和动态数据源切换

💧我们首先创建一个配置类 DataSourceConfig 来配置德鲁伊数据源和动态数据源切换。这个配置类中使用了@Configuration和@Bean注解,定义了两个数据源(主库和从库)和一个动态数据源。动态数据源会根据业务需求自动选择主库还是从库,从而实现了读写分离的功能。这在多数据库场景下非常有用,可以提高数据库的读取性能。

package com.lxr.demo.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
 * 增加了 DataSourceConfig 这个配置文件之后,需要添加druid连接池,单数据源自动装载时不会出这样的问题
 *
 * @Configuration 注解,表明这就是一个配置类,指示一个类声明一个或者多个@Bean 声明的方法并且由Spring容器统一管理,以便在运行时为这些bean生成bean的定义和服务请求的类。
 */
@Configuration
public class DataSourceConfig {
    /**
     * 注入主库数据源
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return new DruidDataSource();
    }
    /**
     * 注入从库数据源
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return new DruidDataSource();
    }
    /**
     * 配置选择数据源
     *
     * @param masterDataSource
     * @param slaveDataSource
     * @return DataSource
     */
    @Bean
    public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DBTypeEnum.MASTER, masterDataSource);
        targetDataSource.put(DBTypeEnum.SLAVE, slaveDataSource);
        MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
        //找不到用默认数据源
        myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
        //可选择目标数据源
        myRoutingDataSource.setTargetDataSources(targetDataSource);
        return myRoutingDataSource;
    }
}

创建自定义注解

💧接下来,我们创建一个自定义注解 Master 来标记我们需要进行主从分离的方法。

package com.lxr.demo.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 有时候主从延迟,需要强制读主库的注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Master {
}

定义数据库读写分离的工具类DBContextHolder

💧这里的 DBContextHolder 是一个线程上下文工具类,通过 ThreadLocal 来实现不同线程使用不同数据源的功能。在实现数据库读写分离的场景下,它可以根据业务需求自动选择主库或从库,确保在多线程环境下的数据源正确切换。这种实现方式非常适用于多线程环境下需要使用读写分离的项目。

package com.lxr.demo.config;
/**
 * ThreadLocal 定义数据源切换,通过ThreadLocal将数据源绑定到每个线程上下文中,
 * ThreadLocal 用来保存每个线程的是使用读库还是写库。操作结束后清除该数据,避免内存泄漏。
 */
public class DBContextHolder {
    /**
     * ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,对数据存储后,只有在当前线程中才可以获取到存储的数据,对于其他线程来说是无法获取到数据。
     * 大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的,通过get和set方法就可以得到当前线程对应的值。
     */
    private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
    public static void set(DBTypeEnum dbTypeEnum) {
        contextHolder.set(dbTypeEnum);
    }
    public static DBTypeEnum get() {
        return contextHolder.get();
    }
    public static void master() {
        set(DBTypeEnum.MASTER);
        System.out.println("--------以下操作为master(操作)--------");
    }
    public static void slave() {
        set(DBTypeEnum.SLAVE);
        System.out.println("--------以下操作为slave(读操作)--------");
    }
    public static void clear() {
        contextHolder.remove();
    }
}

定义枚举类DBTypeEnum

💧这里的 DBTypeEnum 是一个枚举类,用于表示数据库的主库和从库,在数据库读写分离的实现中,可能会用作标识数据源类型的常量,以便在动态数据源切换时选择不同的数据源。这种枚举常量的使用方式有助于代码的可读性和维护性。

package com.lxr.demo.config;
public enum DBTypeEnum {
    MASTER, SLAVE;
}

配置Mybatis指定数据源:SqlSessionFactory和事务管理器

💧这里的MyBatisConfig 是一个Spring配置类,用于配置MyBatis的SqlSessionFactory和事务管理器。通过这个配置类,MyBatis可以连接到动态数据源,并实现数据库的读写分离。同时,启用了事务管理功能,确保在进行数据库操作时能够进行事务控制。

package com.lxr.demo.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
 * 配置Mybatis指定数据源:SqlSessionFactory和事务管理器
 */
@Configuration
@EnableTransactionManagement
public class MyBatisConfig {
    /**
     * 注入自己重写的数据源
     */
    @Resource(name = "myRoutingDataSource")
    private DataSource myRoutingDataSource;
    /**
     * 配置SqlSessionFactory
     *
     * @return SqlSessionFactory
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
        //ResourcePatternResolver(资源查找器)定义了getResources来查找资源
        //PathMatchingResourcePatternResolver提供了以classpath开头的通配符方式查询,否则会调用ResourceLoader的getResource方法来查找
//        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
//        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocation));
        return sqlSessionFactoryBean.getObject();
    }
    /**
     * 事务管理器,不写则事务不生效:事务需要知道当前使用的是哪个数据源才能进行事务处理
     */
    @Bean
    public PlatformTransactionManager platformTransactionManager() {
        return new DataSourceTransactionManager(myRoutingDataSource);
    }
//    /**
//     * 当自定义数据源,用户必须覆盖SqlSessionTemplate,开启BATCH处理模式
//     *
//     * @param sqlSessionFactory
//     * @return
//     */
//    @Bean
//    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
//        return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
//    }
}

自定义数据源路由类MyRoutingDataSource

💧这里的MyRoutingDataSource 是一个自定义的数据源路由类,继承了 AbstractRoutingDataSource 类。它通过重写 determineCurrentLookupKey() 方法,动态决定使用哪个数据源,从而实现了数据库的读写分离。这种动态数据源切换的方式非常灵活,可以根据业务需求在运行时动态选择不同的数据源。

package com.lxr.demo.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
/**
 * 重写 determineCurrentLookupKey 方法,获取当前线程上绑定的路由key。Spring 在开始进行数据库操作时会通过这个方法来决定使用哪个数据库源,因此我们在这里调用上面 DbContextHolder 类的getDbType()方法获取当前操作类别。
 *
 * AbstractRoutingDataSource的getConnection() 方法根据查找 lookup key 键对不同目标数据源的调用,通常是通过(但不一定)某些线程绑定的事物上下文来实现。
 *
 * AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。
 *
 * 基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源。
 */
public class MyRoutingDataSource extends AbstractRoutingDataSource {
    /**
     * determineCurrentLookupKey()方法决定使用哪个数据源、
     * 根据Key获取数据源的信息,上层抽象函数的钩子
     */
    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return DBContextHolder.get();
    }
}

config配置类总结

💧上面介绍了config中的各种配置类以及相关工具类,现在对它们进行简单梳理 ↓

  • DBTypeEnum:这是一个枚举类,定义了两个枚举常量 MASTER 和 SLAVE,分别表示数据库的主库和从库。
  • DBContextHolder:这是一个工具类,使用了 ThreadLocal 来定义数据源切换。它可以将数据源与每个线程的上下文绑定在一起,用于在多线程环境下实现不同线程使用不同的数据源。
  • DataSourceConfig:这是一个Spring配置类,用于配置数据源。它定义了两个 @Bean 方法,分别用于创建主库数据源和从库数据源。此外,还定义了一个 myRoutingDataSource 方法,用于创建一个动态数据源,根据不同的数据源类型选择相应的数据源。
  • MyRoutingDataSource:这是一个自定义的数据源路由类,继承了 AbstractRoutingDataSource 类。它重写了 determineCurrentLookupKey() 方法,用于动态决定当前使用的数据源,根据 DBContextHolder 中存储的数据源类型(主库或从库),选择相应的数据源。
  • MyBatisConfig:这是一个Spring配置类,用于配置MyBatis的 SqlSessionFactory 和事务管理器。它通过 @Resource 注解将 myRoutingDataSource 自动注入,将动态数据源应用到MyBatis框架中。
  • DataSourceAop:这是一个切面类,用于在使用自定义注解时拦截方法调用。它在 before 方法中根据方法上的自定义注解,决定将当前线程的数据源设置为主库或从库,从而实现读写分离的功能。

💧这些类共同实现了一个数据库读写分离的功能。DBTypeEnum 定义了数据源类型,DBContextHolder 管理当前线程的数据源类型,DataSourceConfig 配置多个数据源和动态数据源切换,MyRoutingDataSource 实现数据源的动态路由,MyBatisConfig 将动态数据源应用到MyBatis框架中,DataSourceAop 切面根据方法上的注解选择数据源类型。这种组合使得我们可以在一个Spring Boot项目中实现数据库读写分离的功能。

【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战(下):https://developer.aliyun.com/article/1390150?spm=a2c6h.13148508.setting.22.4fea4f0ervlqra

相关文章
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
929 0
|
监控 安全 Java
Spring AOP实现原理
本内容主要介绍了Spring AOP的核心概念、实现机制及代理生成流程。涵盖切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)等关键概念,解析了JDK动态代理与CGLIB代理的原理及对比,并深入探讨了通知执行链路和责任链模式的应用。同时,详细分析了AspectJ注解驱动的AOP解析过程,包括切面识别、切点表达式匹配及通知适配为Advice的机制,帮助理解Spring AOP的工作原理与实现细节。
1771 13
|
11月前
|
XML Java 数据格式
《深入理解Spring》:AOP面向切面编程深度解析
Spring AOP通过代理模式实现面向切面编程,将日志、事务等横切关注点与业务逻辑分离。支持注解、XML和编程式配置,提供五种通知类型及丰富切点表达式,助力构建高内聚、低耦合的可维护系统。
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
1542 0
|
Java API 微服务
微服务——SpringBoot使用归纳——Spring Boot中的切面AOP处理——Spring Boot 中的 AOP 处理
本文详细讲解了Spring Boot中的AOP(面向切面编程)处理方法。首先介绍如何引入AOP依赖,通过添加`spring-boot-starter-aop`实现。接着阐述了如何定义和实现AOP切面,包括常用注解如`@Aspect`、`@Pointcut`、`@Before`、`@After`、`@AfterReturning`和`@AfterThrowing`的使用场景与示例代码。通过这些注解,可以分别在方法执行前、后、返回时或抛出异常时插入自定义逻辑,从而实现功能增强或日志记录等操作。最后总结了AOP在实际项目中的重要作用,并提供了课程源码下载链接供进一步学习。
1889 0
|
11月前
|
监控 Java Spring
AOP 切面编程
AOP(面向切面编程)通过动态代理在不修改源码的前提下,对方法进行增强。核心概念包括连接点、通知、切入点、切面和目标对象。常用于日志记录、权限校验、性能监控等场景,结合Spring AOP与@Aspect、@Pointcut等注解,实现灵活的横切逻辑管理。
2495 6
AOP 切面编程
|
监控 Java Spring
AOP切面编程快速入门
AOP(面向切面编程)通过分离共性逻辑,简化代码、减少冗余。它通过切点匹配目标方法,在不修改原方法的前提下实现功能增强,如日志记录、性能监控等。核心概念包括:连接点、通知、切入点、切面和目标对象。Spring AOP支持多种通知类型,如前置、后置、环绕、返回后、异常通知,灵活控制方法执行流程。通过@Pointcut可复用切点表达式,提升维护性。此外,结合自定义注解,可实现更清晰的切面控制。
861 5
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
AOP(面向切面编程)能够帮助我们在不修改现有代码的前提下,为应用程序添加新的功能或行为。Micronaut框架中的AOP模块通过动态代理机制实现了这一目标。AOP将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高模块化程度。在Micronaut中,带有特定注解的类会在启动时生成代理对象,在运行时拦截方法调用并执行额外逻辑。例如,可以通过创建切面类并在目标类上添加注解来记录方法调用信息,从而在不侵入原有代码的情况下增强应用功能,提高代码的可维护性和可扩展性。
416 2