SpringBoot:多数据源配置——注解+AOP

2024-06-21 02:48

本文主要是介绍SpringBoot:多数据源配置——注解+AOP,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

* maven依赖

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.1.RELEASE</version><relativePath/> <!-- lookup parent from repository -->
</parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 整合freemarker --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-freemarker</artifactId></dependency><!-- log4j --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j</artifactId></dependency><!-- aop --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- fastJson --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.32</version></dependency><!-- lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- mybatis --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.1.1</version></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>
</dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>
</build>

一,多数据源配置——注解+AOP

    前一篇基于拆包配置维度对多数据源配置进行了简单实现。两种方式对比来看,拆包方式规范性更强,而注解方式更加注重灵活性。通过AOP方式,直接反射获取自定义注解,解析注解值进行数据源动态添加,实现多数据源配置。

二,基于AOP配置流程;相对拆包流程比较复杂,先对流程进行梳理,然后按照流程一步步实现

    * Java整体结构

    * 动态多数据源配置

        -- DataSourceConfig

    * 创建线程持有数据库上下文

        -- DynamicDataSourceHolder

    * 基于Spring提供的AbstractRoutingDataSource,动态添加数据源(事务下可能存在问题)

        -- DynamicDataSource

    * 自定义注解,标识数据源

        -- TargetDataSource

    * AOP前后置拦截解析类,对Mapper方法代用进行拦截

        -- DataSourceAspect

    * 三层代码架构处理

        -- DataSourceAOPController,DataSourceAOPService,DataSourceMapper

三,代码变现

0,application.properties

    * 不同于分数据源配置,单一数据源配置,jdbc-url为url

### mapper存储路径_AOP
mybatis.mapper-locations=classpath:com.gupao.springboot.*.mapper/*.xml### MYSQL_First数据源配置
spring.datasource.first.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.first.jdbc-url=jdbc:mysql://localhost:3306/first?characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.first.username=root
spring.datasource.first.password=123456### MYSQL_First数据源配置
spring.datasource.second.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.second.jdbc-url=jdbc:mysql://localhost:3306/second?characterEncoding=utf-8&serverTimezone=GMT%2B8
spring.datasource.second.username=root
spring.datasource.second.password=123456

1,动态多数据源配置

package com.gupao.springboot.datasourceaop.config;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;/*** 配置数据源* @author pj_zhang* @create 2018-12-28 12:03**/
@Configuration
public class DataSourceConfig {/*** First数据源* @return*/@Bean(name = "firstAopDataSource")@ConfigurationProperties(prefix = "spring.datasource.first")public DataSource firstDataSource() {return DataSourceBuilder.create().build();}/*** Second数据源* @return*/@Bean(name = "secondAopDataSource")@ConfigurationProperties(prefix = "spring.datasource.second")public DataSource secondDataSource() {return DataSourceBuilder.create().build();}/*** 获取动态数据源* @return*/@Bean(name = "dynamicDataSource")@Primarypublic DataSource dynamicDataSource() {DynamicDataSource dynamicDataSource = new DynamicDataSource();// 设置默认数据源为first数据源dynamicDataSource.setDefaultTargetDataSource(firstDataSource());// 配置多数据源, // 添加数据源标识和DataSource引用到目标源映射Map<Object, Object> dataSourceMap = new HashMap<>();dataSourceMap.put("firstAopDataSource", firstDataSource());dataSourceMap.put("secondAopDataSource", secondDataSource());dynamicDataSource.setTargetDataSources(dataSourceMap);return dynamicDataSource;}@Beanpublic PlatformTransactionManager transactionManager() {return new DataSourceTransactionManager(dynamicDataSource());}}

2,创建线程持有数据库上下文,添加数据源到ThreadLocal中

package com.gupao.springboot.datasourceaop.context;/*** 线程持有数据源上下文** @author pj_zhang* @create 2018-12-28 12:00**/
public class DynamicDataSourceHolder {private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>();/*** 设置线程持有的DataSource, 底层以map形式呈现, key为当前线程** @param dataSource*/public static void setDataSource(String dataSource) {THREAD_LOCAL.set(dataSource);}/*** 获取线程持有的当前数据源** @return*/public static String getDataSource() {return THREAD_LOCAL.get();}/*** 清除数据源*/public static void clear() {THREAD_LOCAL.remove();}}

3,基于Spring提供的AbstractRoutingDataSource,动态添加数据源(事务下可能存在问题)

package com.gupao.springboot.datasourceaop.config;import com.gupao.springboot.datasourceaop.context.DynamicDataSourceHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;/*** spring为我们提供了AbstractRoutingDataSource,即带路由的数据源。* 继承后我们需要实现它的determineCurrentLookupKey(),* 该方法用于自定义实际数据源名称的路由选择方法,* 由于我们将信息保存到了ThreadLocal中,所以只需要从中拿出来即可。* @author pj_zhang* @create 2018-12-28 12:04**/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource  {@Overrideprotected Object determineCurrentLookupKey() {// 直接从ThreadLocal中获取拿到的数据源log.info("DynamicDataSource.determineCurrentLookupKey curr data source :" + DynamicDataSourceHolder.getDataSource());return DynamicDataSourceHolder.getDataSource();}
}

4,自定义注解,标识数据源

package com.gupao.springboot.datasourceaop.annotations;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author pj_zhang* @create 2018-12-28 12:13**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TargetDataSource {// 数据源名称String value() default "";
}

5,AOP前后置拦截解析类,对Mapper方法代用进行拦截

package com.gupao.springboot.datasourceaop.aspect;import com.gupao.springboot.datasourceaop.annotations.TargetDataSource;
import com.gupao.springboot.datasourceaop.context.DynamicDataSourceHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
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.core.annotation.Order;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;/*** 多数据源配置, 拦截器配置* @author pj_zhang* @create 2018-12-28 12:15**/
@Aspect
@Component
@Slf4j
// 优先级, 1标识最先执行
@Order(1)
public class DataSourceAspect {private final String DEFAULT_DATA_SOURCE = "firstAopDataSource";@Pointcut("execution(public * com.gupao.springboot.*.mapper.*.*(..))")public void dataSourcePoint() {}@Before("dataSourcePoint()")public void before(JoinPoint joinPoint) {Object target = joinPoint.getTarget();MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();// 执行方法名String methodName = methodSignature.getName();// 方法参数Class[] parameterTypes = methodSignature.getParameterTypes();try {// 获取方法, 直接getClass获取对象可能为代理对象Method method = target.getClass().getInterfaces()[0].getMethod(methodName, parameterTypes);// 添加默认数据源String dataSource = DEFAULT_DATA_SOURCE;if (null != method && method.isAnnotationPresent(TargetDataSource.class)) {TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);dataSource = targetDataSource.value();}// 此处添加线程对应的数据源到上下文// 在AbstractRoutingDataSource子类中拿到数据源, 加载后进行配置DynamicDataSourceHolder.setDataSource(dataSource);log.info("generate data source : " + dataSource);} catch (Exception e) {log.info("error", e);}}/*** 清除数据源, 方法执行完成后, 清除数据源*/@After("dataSourcePoint()")public void after(JoinPoint joinPoint) {DynamicDataSourceHolder.clear();}}

6,Controller层

package com.gupao.springboot.datasourceaop.controller;import com.gupao.springboot.datasourceaop.service.IDataSourceAOPService;
import com.gupao.springboot.entitys.UserVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** @author pj_zhang* @create 2018-12-28 10:42**/
@Slf4j
@RestController
public class DataSourceAOPController {@Autowiredprivate IDataSourceAOPService dataSourceAOPService;@RequestMapping("/firstAOPInsert")public Integer firstInsert(String userName, String password) {UserVO userVO = new UserVO();userVO.setUserName(userName);userVO.setPassword(password);return dataSourceAOPService.insertFirstUserLst(userVO);}@RequestMapping("/secondAOPInsert")public Integer secondInsert(String userName, String password) {UserVO userVO = new UserVO();userVO.setUserName(userName);userVO.setPassword(password);return dataSourceAOPService.insertSecondUserLst(userVO);}@RequestMapping("/firstAOPSelect")public List<UserVO> findFirstData() {return dataSourceAOPService.findFirstData();}@RequestMapping("/secondAOPSelect")public List<UserVO> findSecondData() {return dataSourceAOPService.findSecondData();}@RequestMapping("/insertFirstAndSecond")public Integer insertFirstAndSecond(String userName, String password) {UserVO userVO = new UserVO();userVO.setUserName(userName);userVO.setPassword(password);return dataSourceAOPService.insertFirstAndSecond(userVO);}}

7,Service层

    * Service接口

package com.gupao.springboot.datasourceaop.service;import com.gupao.springboot.entitys.UserVO;import java.util.List;/*** @author pj_zhang* @create 2018-12-28 10:50**/
public interface IDataSourceAOPService {/*** 新增用户到FIRST* @param userVO* @return*/Integer insertFirstUserLst(UserVO userVO);/*** 新增用户到SECEND* @param userVO* @return*/Integer insertSecondUserLst(UserVO userVO);/*** 查找数据FIRST* @return*/List<UserVO> findFirstData();/*** 查找数据SECOND* @return*/List<UserVO> findSecondData();/*** 新增数据到FIRST_SECOND* @param userVO* @return*/Integer insertFirstAndSecond(UserVO userVO);
}

    * Service.Impl

package com.gupao.springboot.datasourceaop.service.impl;import com.gupao.springboot.datasourceaop.mapper.DataSourceMapper;
import com.gupao.springboot.datasourceaop.service.IDataSourceAOPService;
import com.gupao.springboot.entitys.UserVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;/*** @author pj_zhang* @create 2018-12-28 10:43**/
@Slf4j
@Service
public class DataSourceAOPService implements IDataSourceAOPService {@Autowiredprivate DataSourceMapper dataSourceMapper;@Overridepublic Integer insertFirstUserLst(UserVO userVO) {return dataSourceMapper.insertFirstUser(userVO);}@Overridepublic Integer insertSecondUserLst(UserVO userVO) {return dataSourceMapper.insertSecondUser(userVO);}@Overridepublic List<UserVO> findFirstData() {return dataSourceMapper.findFirstData();}@Overridepublic List<UserVO> findSecondData() {return dataSourceMapper.findSecondData();}@Overridepublic Integer insertFirstAndSecond(UserVO userVO) {dataSourceMapper.insertFirstUser(userVO);dataSourceMapper.insertSecondUser(userVO);return 1;}}

8,Mapper层

package com.gupao.springboot.datasourceaop.mapper;import com.gupao.springboot.datasourceaop.annotations.TargetDataSource;
import com.gupao.springboot.entitys.UserVO;
import org.apache.ibatis.annotations.Mapper;import java.util.List;/*** 动态加载数据源* value值为DataSource源数据配置map映射的key值* @author pj_zhang* @create 2018-12-28 10:43**/
@Mapper
public interface DataSourceMapper {/*** 注解为FIRST数据库* @param userVO* @return*/@TargetDataSource("firstAopDataSource")Integer insertFirstUser(UserVO userVO);/*** 注解为SECOND数据库* @param userVO* @return*/@TargetDataSource("secondAopDataSource")Integer insertSecondUser(UserVO userVO);@TargetDataSource("firstAopDataSource")List<UserVO> findFirstData();@TargetDataSource("secondAopDataSource")List<UserVO> findSecondData();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" ><mapper namespace="com.gupao.springboot.datasourceaop.mapper.DataSourceMapper"><insert id="insertFirstUser" parameterType="com.gupao.springboot.entitys.UserVO">INSERT INTOUSER_T(USER, PASSWORD)VALUES (#{userName, jdbcType=VARCHAR},#{password, jdbcType=VARCHAR})</insert><insert id="insertSecondUser" parameterType="com.gupao.springboot.entitys.UserVO">INSERT INTOUSER_T(USER, PASSWORD)VALUES (#{userName, jdbcType=VARCHAR},#{password, jdbcType=VARCHAR})</insert><select id="findFirstData" resultType="com.gupao.springboot.entitys.UserVO">SELECTuser as userName,password as passwordfromUSER_T</select><select id="findSecondData" resultType="com.gupao.springboot.entitys.UserVO">SELECTuser as userName,password as passwordfromUSER_T</select></mapper>

9,启动入库

package com.gupao.springboot;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
// 去除SpringBoot自动配置, 采用自定义数据源配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GupaoSpringbootApplication {public static void main(String[] args) {SpringApplication.run(GupaoSpringbootApplication.class, args);}}

10,测试

    * FIRST入库数据

    * 注意两条日志打印顺序;先通过AOP变更了数据源,再通过实现类进行数据源加载,后续同!

    * SECOND入库数据

    * FIRST+SECOND入库数据

    * FIRST查询数据

    * SECOND查询数据

11,数据库数据

    * FIRST

    * SECOND

四,存在问题

1,踩过的一个坑,数据库连接错误,非正常错误

    error :java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone val

    这个问题可能是数据库时区问题导致的,具体解决措施在jdbc-url后面加上参数,如下

    spring.datasource.first.jdbc-url=jdbc:mysql://localhost:3306/first?characterEncoding=utf-8&serverTimezone=GMT%2B8

2,AOP配置引起的数据源加载问题

    * 基于AbstractRoutingDataSource实现类配置的数据源动态加载,依赖于程序的执行顺序。先通过Mapper方法调用变更DynamicDataSourceHolder上下文持有的DataSource再进行数据源加载时,此时配置没有任何问题;

    * 但是单纯添加了声明式事务后,因为事务执行流程影响,AbstractRoutingDataSource实现类会先于Mapper方法执行,此时DynamicDataSourceHolder上下文并不持有数据源,则数据源为DataSourceConfig中配置的默认数据源;

    * 鉴于上面存在的问题,有的配置方式不基于AbstractRoutingDataSource实现类去动态加载数据源,而是在AOP前置拦截方法中,拦截到注解的数据源后,直接从Spring容器中获取DataSource并进行更改,直接跳过执行顺序可能存在的影响,该配置后续会继续完善在后面!

这篇关于SpringBoot:多数据源配置——注解+AOP的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1079997

相关文章

Java五子棋之坐标校正

上篇针对了Java项目中的解构思维,在这篇内容中我们不妨从整体项目中拆解拿出一个非常重要的五子棋逻辑实现:坐标校正,我们如何使漫无目的鼠标点击变得有序化和可控化呢? 目录 一、从鼠标监听到获取坐标 1.MouseListener和MouseAdapter 2.mousePressed方法 二、坐标校正的具体实现方法 1.关于fillOval方法 2.坐标获取 3.坐标转换 4.坐

Spring Cloud:构建分布式系统的利器

引言 在当今的云计算和微服务架构时代,构建高效、可靠的分布式系统成为软件开发的重要任务。Spring Cloud 提供了一套完整的解决方案,帮助开发者快速构建分布式系统中的一些常见模式(例如配置管理、服务发现、断路器等)。本文将探讨 Spring Cloud 的定义、核心组件、应用场景以及未来的发展趋势。 什么是 Spring Cloud Spring Cloud 是一个基于 Spring

Linux 安装、配置Tomcat 的HTTPS

Linux 安装 、配置Tomcat的HTTPS 安装Tomcat 这里选择的是 tomcat 10.X ,需要Java 11及更高版本 Binary Distributions ->Core->选择 tar.gz包 下载、上传到内网服务器 /opt 目录tar -xzf 解压将解压的根目录改名为 tomat-10 并移动到 /opt 下, 形成个人习惯的路径 /opt/tomcat-10

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值:简单的数据即基础数据类型,按值访问。 引用值:由多个值构成的对象即复杂数据类型,按引用访问。 动态属性 对于引用值而言,可以随时添加、修改和删除其属性和方法。 let person = new Object();person.name = 'Jason';person.age = 42;console.log(person.name,person.age);//'J

java8的新特性之一(Java Lambda表达式)

1:Java8的新特性 Lambda 表达式: 允许以更简洁的方式表示匿名函数(或称为闭包)。可以将Lambda表达式作为参数传递给方法或赋值给函数式接口类型的变量。 Stream API: 提供了一种处理集合数据的流式处理方式,支持函数式编程风格。 允许以声明性方式处理数据集合(如List、Set等)。提供了一系列操作,如map、filter、reduce等,以支持复杂的查询和转

uniapp接入微信小程序原生代码配置方案(优化版)

uniapp项目需要把微信小程序原生语法的功能代码嵌套过来,无需把原生代码转换为uniapp,可以配置拷贝的方式集成过来 1、拷贝代码包到src目录 2、vue.config.js中配置原生代码包直接拷贝到编译目录中 3、pages.json中配置分包目录,原生入口组件的路径 4、manifest.json中配置分包,使用原生组件 5、需要把原生代码包里的页面修改成组件的方

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

详细分析Springmvc中的@ModelAttribute基本知识(附Demo)

目录 前言1. 注解用法1.1 方法参数1.2 方法1.3 类 2. 注解场景2.1 表单参数2.2 AJAX请求2.3 文件上传 3. 实战4. 总结 前言 将请求参数绑定到模型对象上,或者在请求处理之前添加模型属性 可以在方法参数、方法或者类上使用 一般适用这几种场景: 表单处理:通过 @ModelAttribute 将表单数据绑定到模型对象上预处理逻辑:在请求处理之前

eclipse运行springboot项目,找不到主类

解决办法尝试了很多种,下载sts压缩包行不通。最后解决办法如图: help--->Eclipse Marketplace--->Popular--->找到Spring Tools 3---->Installed。

JAVA读取MongoDB中的二进制图片并显示在页面上

1:Jsp页面: <td><img src="${ctx}/mongoImg/show"></td> 2:xml配置: <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001