MyBatis 使用手册
发布日期:2021-05-09 04:36:17 浏览次数:18 分类:博客文章

本文共 46923 字,大约阅读时间需要 156 分钟。

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

越来越多的企业已经将 MyBatis 使用到了正式的生产环境,本文就使用 MyBatis 的几种方式提供简单的示例,以及如何对数据库密码进行加密,目前有以下章节:

  • 单独使用 MyBatis
  • 集成 Spring 框架
  • 集成 Spring Boot 框架
  • Spring Boot 配置数据库密码加密
  • 如何配置动态数据源

1.单独使用

引入 MyBatis 依赖,单独使用,版本是3.5.6

引入依赖

org.mybatis
mybatis
3.5.6
mysql
mysql-connector-java
8.0.19

添加mybatis-config.xml

开始使用

Model类

tk.mybatis.simple.model.DbTest.java

package tk.mybatis.simple.model;public class DbTest {    public Integer id;    public String text;    public Integer getId() {        return id;    }    public void setId(Integer id) {        this.id = id;    }    public String getText() {        return text;    }    public void setText(String text) {        this.text = text;    }    @Override    public String toString() {        return "DbTest{" +                "id=" + id +                ", text='" + text + '\'' +                '}';    }}

Mapper接口

tk.mybatis.simple.mapper.DbTestMapper

package tk.mybatis.simple.mapper;import tk.mybatis.simple.model.DbTest;public interface DbTestMapper {    DbTest queryById(Integer id);}

XML映射文件

XML映射文件请放于Mapper接口所在路径下,保证名称相同

tk/mybatis/simple/mapper/DbTestMapper.xml

执行示例

package tk.mybatis.simple;import org.apache.ibatis.io.Resources;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;import tk.mybatis.simple.mapper.DbTestMapper;import java.io.IOException;import java.io.Reader;public class MyBatisHelper {    private static SqlSessionFactory sqlSessionFactory;    static {        try {            // MyBatis的配置文件            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");            // 创建一个 sqlSessionFactory 工厂类            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);            reader.close();        } catch (IOException e) {            e.printStackTrace();        }    }    public static SqlSession getSqlSession() {        return sqlSessionFactory.openSession();    }    public static void main(String[] args) {        // 创建一个 SqlSession 会话        SqlSession sqlSession = MyBatisHelper.getSqlSession();        // 获取 DbTestMapper 接口的动态代理对象        DbTestMapper dbTestMapper = sqlSession.getMapper(DbTestMapper.class);        // 执行查询        System.out.println(dbTestMapper.queryById(1).toString());    }}

2.集成Spring

在 Spring 项目中,使用 MyBatis 的模板,请注意 Spring 的版本为5.2.10.RELEASE

日志框架使用(推荐),版本为2.13.3,性能高于logback和log4j

工程结构图

  • 其中jquery.min.js文件可以去官网下载

引入依赖

4.0.0
tk.mybatis.simple
mybatis-spring
war
3.5.6
2.11.3
1.8
UTF-8
org.springframework
spring-framework-bom
5.2.10.RELEASE
pom
import
org.apache.logging.log4j
log4j-bom
2.13.3
import
pom
org.mybatis
mybatis-spring
1.3.2
org.mybatis
mybatis
${mybatis.version}
mysql
mysql-connector-java
8.0.19
com.alibaba
druid
1.2.3
com.github.pagehelper
pagehelper
5.2.0
com.github.jsqlparser
jsqlparser
3.2
javax.servlet
servlet-api
2.5
provided
javax.servlet.jsp
jsp-api
2.1
provided
javax.servlet
jstl
1.2
org.springframework
spring-context
org.springframework
spring-jdbc
org.springframework
spring-tx
org.springframework
spring-aop
org.aspectj
aspectjweaver
1.8.2
javax.annotation
javax.annotation-api
1.2
javax.transaction
javax.transaction-api
1.2
org.springframework
spring-web
org.springframework
spring-webmvc
com.fasterxml.jackson.core
jackson-databind
${jackson.version}
com.fasterxml.jackson.core
jackson-core
${jackson.version}
com.fasterxml.jackson.core
jackson-annotations
${jackson.version}
org.apache.logging.log4j
log4j-core
org.apache.logging.log4j
log4j-api
org.apache.logging.log4j
log4j-web
org.apache.logging.log4j
log4j-slf4j-impl
org.apache.logging.log4j
log4j-1.2-api
org.apache.logging.log4j
log4j-jcl
org.slf4j
slf4j-api
1.7.7
com.lmax
disruptor
3.4.2
commons-fileupload
commons-fileupload
1.4
commons-io
commons-io
2.8.0
commons-codec
commons-codec
1.15
study-ssm
maven-compiler-plugin
1.8
1.8
UTF-8
src/main/java
**/*.properties
**/*.xml
false
src/main/resources/META-INF/spring
spring-mybatis.xml
spring-mvc.xml
false
src/main/resources
**/*.properties
**/*.xml
false

添加spring-mvc.xml

text/html;charset=UTF-8
json=application/json xml=application/xml html=text/html

添加mybatis-config.xml

添加spring-mybatis.xml

添加log4j2.xml

添加web.xml

Archetype Created Web Application
contextConfigLocation
classpath:spring-mybatis.xml
encodingFilter
org.springframework.web.filter.CharacterEncodingFilter
true
encoding
UTF-8
forceEncoding
true
encodingFilter
/*
org.springframework.web.context.ContextLoaderListener
org.springframework.web.util.IntrospectorCleanupListener
SpringMVC
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring-mvc.xml
1
true
SpringMVC
/
index.jsp

开始使用

Model类

tk.mybatis.simple.model.DbTest.java

package tk.mybatis.simple.model;public class DbTest {    public Integer id;    public String text;    public Integer getId() {        return id;    }    public void setId(Integer id) {        this.id = id;    }    public String getText() {        return text;    }    public void setText(String text) {        this.text = text;    }    @Override    public String toString() {        return "DbTest{" +                "id=" + id +                ", text='" + text + '\'' +                '}';    }}

Mapper接口

tk.mybatis.simple.mapper.DbTestMapper

package tk.mybatis.simple.mapper;import tk.mybatis.simple.model.DbTest;public interface DbTestMapper {    DbTest queryById(Integer id);}

XML映射文件

XML映射文件请放于Mapper接口所在路径下,保证名称相同

tk/mybatis/simple/mapper/DbTestMapper.xml

Controller类

package tk.mybatis.simple.controller;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.servlet.ModelAndView;import tk.mybatis.simple.mapper.DbTestMapper;import tk.mybatis.simple.model.DbTest;@Controller@RequestMapping(value = "/test")public class DbTestController {    private static final Logger logger = LogManager.getLogger(DbTestController.class);    @Autowired    private DbTestMapper dbTestMapper;    @GetMapping("/")    public String index() {        return "welcome";    }    @GetMapping("/query")    public ModelAndView query(@RequestParam("id") Integer id) {        DbTest dbTest = dbTestMapper.queryById(id);        logger.info("入参:{},查询结果:{}", id, dbTest.toString());        ModelAndView mv = new ModelAndView();        mv.setViewName("result");        mv.addObject("test", dbTest);        return mv;    }}

index.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ page contentType="text/html;charset=UTF-8" language="java" %>    
首页

Hello World!

">

welcome.jsp

<%--  Created by IntelliJ IDEA.  User: jingp  Date: 2019/6/5  Time: 15:17  To change this template use File | Settings | File Templates.--%><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ page contentType="text/html;charset=UTF-8" language="java" %>    
查询

查询数据库

">
ID:

result.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><%@ page contentType="text/html;charset=UTF-8" language="java" %>    
结果

查询结果:${test.toString()}

">

运行程序

配置好Tomcat,然后启动就可以了,进入页面,点击开始测试,然后查询数据库就可以通过MyBatis操作数据库了

3.集成SpringBoot

引入依赖

org.springframework.boot
spring-boot-starter-parent
2.0.3.RELEASE
4.0.0
tk.mybatis.simple
mybatis-spring-boot
jar
1.8
UTF-8
org.apache.logging.log4j
log4j-bom
2.13.3
import
pom
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.projectlombok
lombok
1.16.22
provided
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.4
mysql
mysql-connector-java
8.0.19
com.alibaba
druid-spring-boot-starter
1.2.3
com.github.pagehelper
pagehelper-spring-boot-starter
1.3.0
org.apache.logging.log4j
log4j-core
org.apache.logging.log4j
log4j-api
org.apache.logging.log4j
log4j-web
org.apache.logging.log4j
log4j-slf4j-impl
org.apache.logging.log4j
log4j-1.2-api
org.apache.logging.log4j
log4j-jcl
org.slf4j
slf4j-api
1.7.7
com.lmax
disruptor
3.4.2
com.alibaba
fastjson
1.2.54
basic
org.codehaus.mojo
appassembler-maven-plugin
1.10
unix
windows
lib
flat
conf
true
src/main/resources
true
${project.build.directory}/basic-assemble
com.fullmoon.study.Application
basic
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/app/dump
-Xmx512m
-Xms512m
org.apache.maven.plugins
maven-compiler-plugin
1.8
1.8
src/main/resources
*.properties
*.xml
*.yml
false
src/main/resources/mapper
mapper/
*.xml
false

添加mybatis-config.xml

MyBatis 的配置文件

添加application.yml

Spring Boot 的配置文件

server:  port: 9092  servlet:    context-path: /mybatis-spring-boot-demo  tomcat:    accept-count: 200    min-spare-threads: 200spring:  application:    name: mybatis-spring-boot-demo  profiles:    active: test  servlet:    multipart:      max-file-size: 100MB      max-request-size: 100MB  datasource:    type: com.alibaba.druid.pool.DruidDataSource    druid:      driver-class-name: com.mysql.cj.jdbc.Driver # 不配置则会根据 url 自动识别      initial-size: 5 # 初始化时建立物理连接的个数      min-idle: 20 # 最小连接池数量      max-active: 20 # 最大连接池数量      max-wait: 10000 # 获取连接时最大等待时间,单位毫秒      validation-query: SELECT 1 # 用来检测连接是否有效的 sql      test-while-idle: true # 申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,则执行 validationQuery 检测连接是否有效      test-on-borrow: false # 申请连接时执行 validationQuery 检测连接是否有效      min-evictable-idle-time-millis: 120000 # 连接保持空闲而不被驱逐的最小时间,单位是毫秒      time-between-eviction-runs-millis: 3600000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒      filters: stat,log4j,wall # 配置过滤器,stat-监控统计,log4j-日志,wall-防御 SQL 注入      connection-properties: 'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000' # StatFilter配置,打开合并 SQL 功能和慢 SQL 记录      web-stat-filter: # WebStatFilter 配置        enabled: true        url-pattern: '/*'        exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*'      stat-view-servlet: # StatViewServlet 配置        enabled: true        url-pattern: '/druid/*'        reset-enable: false        login-username: druid        login-password: druid      aop-patterns: 'com.fullmoon.study.service.*' # Spring 监控配置mybatis:  type-aliases-package: tk.mybatis.simple.model  mapper-locations: classpath:mapper/*.xml  config-location: classpath:mybatis-config.xmlpagehelper:  helper-dialect: mysql  reasonable: true # 分页合理化参数  offset-as-page-num: true # 将 RowBounds 中的 offset 参数当成 pageNum 使用  supportMethodsArguments: true # 支持通过 Mapper 接口参数来传递分页参数# 测试环境---spring:  profiles: test  datasource:    druid:      url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8      username: root      password: password

注意上面 mybatis 的相关配置,XML 映射文件是存放于 classpath 下的 mapper 文件夹下面的

添加log4j2.xml

开始使用

在 Spring Boot 项目的启动类上面添加 @MapperScan("tk.mybatis.simple.mapper") 注解,指定 Mapper 接口所在的包路径,启动类如下:

@SpringBootApplication@EnableTransactionManagement@MapperScan("tk.mybatis.simple.mapper")public class Application {    public static void main(String[] args){        SpringApplication.run(Application.class, args);    }}

然后在 classpath 下的 mapper 文件夹(根据 application.yml 配置文件中的定义)添加 XML 映射文件,即可开始使用 MyBatis了

其中 @EnableTransactionManagement 注解表示开启事务的支持(@SpringBootApplication 注解在加载容器的时候已经开启事务管理的功能了,也可不需要添加该注解)

在需要事务的方法或者类上面添加 @Transactional 注解即可,引入spring-boot-starter-jdbc依赖,注入的是 DataSourceTransactionManager 事务管理器,事务的使用示例如下:

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)public int insertFeedback(UserFeedbackInfo requestAddParam) {    try {        // ... 业务逻辑     } catch (Exception e) {        // 事务回滚        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();        return 0;    }}

4.SpringBoot配置数据库密码加密?

4.1 借助Druid数据源

Druid 数据源支持数据库密码进行加密,在 Spring Boot 中配置方式如下:

加密数据库密码,通过 Druid 的 com.alibaba.druid.filter.config.ConfigTools 工具类对数据库密码进行加密(RSA 算法),如下:

public static void main(String[] args) throws Exception {    ConfigTools.main(new String[]{"you_password"});}

或者执行以下命令:

java -cp druid-1.0.16.jar com.alibaba.druid.filter.config.ConfigTools you_password

输出:

privateKey:MIIBVgIBADANBgkqhkiG9w0BAQEFAASCAUAwggE8AgEAAkEA6+4avFnQKP+O7bu5YnxWoOZjv3no4aFV558HTPDoXs6EGD0HP7RzzhGPOKmpLQ1BbA5viSht+aDdaxXp6SvtMQIDAQABAkAeQt4fBo4SlCTrDUcMANLDtIlax/I87oqsONOg5M2JS0jNSbZuAXDv7/YEGEtMKuIESBZh7pvVG8FV531/fyOZAiEA+POkE+QwVbUfGyeugR6IGvnt4yeOwkC3bUoATScsN98CIQDynBXC8YngDNwZ62QPX+ONpqCel6g8NO9VKC+ETaS87wIhAKRouxZL38PqfqV/WlZ5ZGd0YS9gA360IK8zbOmHEkO/AiEAsES3iuvzQNYXFL3x9Tm2GzT1fkSx9wx+12BbJcVD7AECIQCD3Tv9S+AgRhQoNcuaSDNluVrL/B/wOmJRLqaOVJLQGg==publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAOvuGrxZ0Cj/ju27uWJ8VqDmY7956OGhVeefB0zw6F7OhBg9Bz+0c84RjzipqS0NQWwOb4kobfmg3WsV6ekr7TECAwEAAQ==password:PNak4Yui0+2Ft6JSoKBsgNPl+A033rdLhFw+L0np1o+HDRrCo9VkCuiiXviEMYwUgpHZUFxb2FpE0YmSguuRww==

然后在 application.yml 中添加以下配置:

spring:  datasource:    druid:      password: ${password} # 加密后的数据库密码      filters: config # 配置 ConfigFilter ,通过它进行解密      # 提示需要对数据库密码进行解密      connection-properties: 'config.decrypt=true;config.decrypt.key=${publickey}'publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJvQUB7pExzbzTpaQCCY5qS+86MgYWvRpqPUCzjFwdMgrBjERE5X5oojSe48IGZ6UtCCeaI0PdhkFoNaJednaqMCAwEAAQ==

这样就OK了,主要是在 ConfigFilter 过滤器中,会先对密码进行解密,然后再设置到 DataSource 数据源

4.2 借助Jasypt加密包

Jasypt是一个 Java 库,可以让开发人员将基本的加密功能添加到项目中,而无需对加密的工作原理有深入的了解

接下来讲述的在 Spring Boot 项目中如何使用Jasypt,其他使用方法请参考

引入依赖

com.github.ulisesbocchio
jasypt-spring-boot-starter
3.0.3

添加注解

在启动类上面添加@EnableEncryptableProperties注解,使整个 Spring 环境启用 Jasypt 加密功能,如下:

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

获取密文

需要通过 Jasypt 官方提供的 jar 包进行加密,如下:

import com.ulisesbocchio.jasyptspringboot.properties.JasyptEncryptorConfigurationProperties;import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;import org.jasypt.encryption.pbe.config.PBEConfig;import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;/** * @author jingping.liu * @date 2020-11-20 * @description Jasypt 安全框架加密类工具包 */public class JasyptUtils {    /**     * 生成一个 {@link PBEConfig} 配置对象     * 

* 注意!!! * 可查看 Jasypt 全局配置对象 {@link JasyptEncryptorConfigurationProperties} 中的默认值 * 这里的配置建议与默认值保持一致,否则需要在 application.yml 中定义这里的配置(也可以通过 JVM 参数的方法) * 注意 password 和 algorithm 配置项,如果不一致在启动时可能会解密失败而报错 * * @param salt 盐值 * @return SimpleStringPBEConfig 加密配置 */ private static SimpleStringPBEConfig generatePBEConfig(String salt) { SimpleStringPBEConfig config = new SimpleStringPBEConfig(); // 设置 salt 盐值 config.setPassword(salt); // 设置要创建的加密程序池的大小,这里仅临时创建一个,设置 1 即可 config.setPoolSize("1"); // 设置加密算法, 此算法必须由 JCE 提供程序支持,默认值 PBEWITHHMACSHA512ANDAES_256 config.setAlgorithm("PBEWithMD5AndDES"); // 设置应用于获取加密密钥的哈希迭代次数 config.setKeyObtentionIterations("1000"); // 设置要请求加密算法的安全提供程序的名称 config.setProviderName("SunJCE"); // 设置 salt 盐的生成器 config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator"); // 设置 IV 生成器 config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator"); // 设置字符串输出的编码形式,支持 base64 和 hexadecimal config.setStringOutputType("base64"); return config; } /** * 通过 {@link PooledPBEStringEncryptor} 进行加密密 * * @param salt 盐值 * @param message 需要加密的内容 * @return 加密后的内容 */ public static String encrypt(String salt, String message) { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); SimpleStringPBEConfig pbeConfig = generatePBEConfig(salt); // 生成加密配置 encryptor.setConfig(pbeConfig); System.out.println("----ARGUMENTS-------------------"); System.out.println("message: " + message); System.out.println("salt: " + pbeConfig.getPassword()); System.out.println("algorithm: " + pbeConfig.getAlgorithm()); System.out.println("stringOutputType: " + pbeConfig.getStringOutputType()); // 进行加密 String cipherText = encryptor.encrypt(message); System.out.println("----OUTPUT-------------------"); System.out.println(cipherText); return cipherText; } public static String decrypt(String salt, String message) { PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); // 设置解密配置 encryptor.setConfig(generatePBEConfig(salt)); // 进行解密 return encryptor.decrypt(message); } public static void main(String[] args) { // 随机生成一个 salt 盐值 String salt = UUID.randomUUID().toString().replace("-", ""); // 进行加密 encrypt(salt, "message"); }}

如何使用

直接在 application.yml 配置文件中添加 Jasypt 配置和生成的密文

jasypt:  encryptor:    password: salt # salt 盐值,需要和加密时使用的 salt 一致    algorithm: PBEWithMD5AndDES # 加密算法,需要和加密时使用的算法一致    string-output-type: hexadecimal # 密文格式,,需要和加密时使用的输出格式一致spring:  datasource:    druid:      username: root      password: ENC(cipherText) # Jasypt 密文格式:ENC(密文)

salt 盐值也可以通过 JVM 参数进行设置,例如:-Djasypt.encryptor.password=salt

启动后,Jasypt 会先根据配置将 ENC(密文) 进行解密,然后设置到 Spring 环境中

5.如何配置动态数据源?

你在使用 MyBatis 的过程中,是否有想过多个数据源应该如何配置,如何去实现?出于这个好奇心,我在 的数据库多数据源中知晓 Spring 提供了对多数据源的支持,基于 Spring 提供的 AbstractRoutingDataSource,自己实现数据源的切换,下面就如何配置动态数据源提供一个简单的实现

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,代码如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {	@Nullable	private Object defaultTargetDataSource;	@Nullable	private Map
resolvedDataSources; @Nullable private DataSource resolvedDefaultDataSource; @Override public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); } protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); // 确定当前要使用的数据源 Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; } /** * Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context. *

* Allows for arbitrary keys. The returned key needs to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ @Nullable protected abstract Object determineCurrentLookupKey(); // 省略相关代码...}

重写 AbstractRoutingDataSource 的 determineCurrentLookupKey() 方法,可以实现对多数据源的支持

思路:

  1. 重写其 determineCurrentLookupKey() 方法,支持选择不同的数据源
  2. 初始化多个 DataSource 数据源到 AbstractRoutingDataSource 的 resolvedDataSources 属性中
  3. 然后通过 Spring AOP, 以自定义注解作为切点,根据不同的数据源的 Key 值,设置当前线程使用的数据源

接下来的实现方式是 Spring Boot 结合 Druid 配置动态数据源

引入依赖

基于 3.继承SpringBoot 中已添加的依赖再添加对AOP支持的依赖,如下:

org.springframework.boot
spring-boot-starter-aop

开始实现

DataSourceContextHolder

使用 ThreadLocal 存储当前线程指定的数据源的 Key 值,代码如下:

@Log4j2public class DataSourceContextHolder {    /**     * 线程本地变量     */    private static final ThreadLocal
DATASOURCE_KEY = new ThreadLocal<>(); /** * 配置的所有数据源的 Key 值 */ public static Set
ALL_DATASOURCE_KEY = new HashSet<>(); /** * 设置当前线程的数据源的 Key * * @param dataSourceKey 数据源的 Key 值 */ public static void setDataSourceKey(String dataSourceKey) { if (ALL_DATASOURCE_KEY.contains(dataSourceKey)) { DATASOURCE_KEY.set(dataSourceKey); } else { log.warn("the datasource [{}] does not exist", dataSourceKey); } } /** * 获取当前线程的数据源的 Key 值 * * @return 数据源的 Key 值 */ public static String getDataSourceKey() { return DATASOURCE_KEY.get(); } /** * 移除当前线程持有的数据源的 Key 值 */ public static void clear() { DATASOURCE_KEY.remove(); }}

MultipleDataSource

重写其 AbstractRoutingDataSource 的 determineCurrentLookupKey() 方法,代码如下:

public class MultipleDataSource extends AbstractRoutingDataSource {    /**     * 返回当前线程是有的数据源的 Key     *     * @return dataSourceKey     */    @Override    protected Object determineCurrentLookupKey() {        return DataSourceContextHolder.getDataSourceKey();    }}

@TargetDataSource注解

自定义注解,用于 Spring AOP 通过其设置当前需要使用的数据源,代码如下:

/** * @description 数据源注解,可以用于 Method, Class, interface (including annotation type), or enum declaration */@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD, ElementType.TYPE})public @interface TargetDataSource {    /**     * 指定数据源 {@link DataSourceContextHolder}     *     * @return 数据源的 Key 值     */    String value() default "master";}

DataSourceAspect切面

使用 Spring AOP 功能,定义一个切面,用于设置当前需要使用的数据源,代码如下:

@Aspect@Component@Log4j2public class DataSourceAspect {    @Before("@annotation(com.fullmoon.study.datasource.annotation.TargetDataSource)")    public void before(JoinPoint joinPoint) {        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();        Method method = methodSignature.getMethod();        if (method.isAnnotationPresent(TargetDataSource.class)) {            TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);            DataSourceContextHolder.setDataSourceKey(targetDataSource.value());            log.info("set the datasource of the current thread to [{}]", targetDataSource.value());        } else if (joinPoint.getTarget().getClass().isAnnotationPresent(TargetDataSource.class)) {            TargetDataSource targetDataSource = joinPoint.getTarget().getClass().getAnnotation(TargetDataSource.class);            DataSourceContextHolder.setDataSourceKey(targetDataSource.value());            log.info("set the datasource of the current thread to [{}]", targetDataSource.value());        }    }    @After("@annotation(com.fullmoon.study.datasource.annotation.TargetDataSource)")    public void after() {        DataSourceContextHolder.clear();        log.info("clear the datasource of the current thread");    }}

DruidConfig

Druid 配置类,代码如下:

@Configurationpublic class DruidConfig {    @Bean    public ServletRegistrationBean
statViewServlet() { ServletRegistrationBean
servletRegistrationBean = new ServletRegistrationBean<>(); servletRegistrationBean.setServlet(new StatViewServlet()); servletRegistrationBean.addUrlMappings("/druid/*"); Map
initParameters = new HashMap<>(16); // 禁用HTML页面上的Reset All功能 initParameters.put("resetEnable", "false"); // IP白名单 (没有配置或者为空,则允许所有访问,同一IP共存于deny时,deny优先于allow处理) initParameters.put("allow", ""); // druid监控页面登录用户名 initParameters.put("loginUsername", "druid"); // druid监控页面登录密码 initParameters.put("loginPassword", "druid"); servletRegistrationBean.setInitParameters(initParameters); return servletRegistrationBean; } @Bean public FilterRegistrationBean
webStatFilter() { FilterRegistrationBean
filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(new WebStatFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } @Bean(value = "druid-stat-interceptor") public DruidStatInterceptor druidStatInterceptor() { return new DruidStatInterceptor(); } @Bean public BeanNameAutoProxyCreator beanNameAutoProxyCreator() { BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator(); beanNameAutoProxyCreator.setProxyTargetClass(true); // 设置要监控的bean的id beanNameAutoProxyCreator.setInterceptorNames("druid-stat-interceptor"); return beanNameAutoProxyCreator; }}

MultipleDataSourceConfig

MyBatis 的配置类,配置了 2 个数据源,代码如下:

@Configuration@EnableConfigurationProperties({MybatisProperties.class})public class MultipleDataSourceConfig {    private final MybatisProperties properties;    private final Interceptor[] interceptors;    private final TypeHandler[] typeHandlers;    private final LanguageDriver[] languageDrivers;    private final ResourceLoader resourceLoader;    private final DatabaseIdProvider databaseIdProvider;    private final List
configurationCustomizers; public MultipleDataSourceConfig(MybatisProperties properties, ObjectProvider
interceptorsProvider, ObjectProvider
typeHandlersProvider, ObjectProvider
languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider
databaseIdProvider, ObjectProvider
> configurationCustomizersProvider) { this.properties = properties; this.interceptors = (Interceptor[]) interceptorsProvider.getIfAvailable(); this.typeHandlers = (TypeHandler[]) typeHandlersProvider.getIfAvailable(); this.languageDrivers = (LanguageDriver[]) languageDriversProvider.getIfAvailable(); this.resourceLoader = resourceLoader; this.databaseIdProvider = (DatabaseIdProvider) databaseIdProvider.getIfAvailable(); this.configurationCustomizers = (List) configurationCustomizersProvider.getIfAvailable(); } @Bean(name = "master", initMethod = "init", destroyMethod = "close") @ConfigurationProperties(prefix = "spring.datasource.druid.master") public DruidDataSource master() { return DruidDataSourceBuilder.create().build(); } @Bean(name = "slave", initMethod = "init", destroyMethod = "close") @ConfigurationProperties(prefix = "spring.datasource.druid.slave") public DruidDataSource slave() { return DruidDataSourceBuilder.create().build(); } @Bean(name = "dynamicDataSource") public DataSource dynamicDataSource() { MultipleDataSource dynamicRoutingDataSource = new MultipleDataSource(); Map
dataSources = new HashMap<>(); dataSources.put("master", master()); dataSources.put("slave", slave()); dynamicRoutingDataSource.setDefaultTargetDataSource(master()); dynamicRoutingDataSource.setTargetDataSources(dataSources); DataSourceContextHolder.ALL_DATASOURCE_KEY.addAll(dataSources.keySet()); return dynamicRoutingDataSource; } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dynamicDataSource()); factory.setVfs(SpringBootVFS.class); if (StringUtils.hasText(this.properties.getConfigLocation())) { factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation())); } this.applyConfiguration(factory); if (this.properties.getConfigurationProperties() != null) { factory.setConfigurationProperties(this.properties.getConfigurationProperties()); } if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); } if (this.databaseIdProvider != null) { factory.setDatabaseIdProvider(this.databaseIdProvider); } if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) { factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage()); } if (this.properties.getTypeAliasesSuperType() != null) { factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType()); } if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) { factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage()); } if (!ObjectUtils.isEmpty(this.typeHandlers)) { factory.setTypeHandlers(this.typeHandlers); } if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) { factory.setMapperLocations(this.properties.resolveMapperLocations()); } Set
factoryPropertyNames = (Set) Stream.of((new BeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet()); Class
defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver(); if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) { factory.setScriptingLanguageDrivers(this.languageDrivers); if (defaultLanguageDriver == null && this.languageDrivers.length == 1) { defaultLanguageDriver = this.languageDrivers[0].getClass(); } } if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) { factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver); } return factory.getObject(); } private void applyConfiguration(SqlSessionFactoryBean factory) { org.apache.ibatis.session.Configuration configuration = this.properties.getConfiguration(); if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) { configuration = new org.apache.ibatis.session.Configuration(); } if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) { Iterator var3 = this.configurationCustomizers.iterator(); while (var3.hasNext()) { ConfigurationCustomizer customizer = (ConfigurationCustomizer) var3.next(); customizer.customize(configuration); } } factory.setConfiguration(configuration); } @Bean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory); } @Bean public PlatformTransactionManager masterTransactionManager() { // 配置事务管理器 return new DataSourceTransactionManager(dynamicDataSource()); }}
  1. 定义了两个数据源,配置的前缀分别为spring.datasource.druid.masterspring.datasource.druid.slave,对应 masterslave 两个 DruidDataSource 数据源

  2. 还有一个自定义的 MultipleDataSource 动态数据源,添加上面两个数据源到其中,并设置默认数据源为 master

  3. 其中 SqlSessionFactory 和 SqlSessionTemplate 的创建,借鉴了 mybatis-spring-boot-starterorg.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 自动配置类(直接抄过来的😄),在初始化 SqlSessionFactory 时,设置的数据源就是上面定义的 MultipleDataSource 动态数据源

  4. 另外还为 MultipleDataSource 动态数据源定义了一个事务管理器 DataSourceTransactionManager

添加配置

server:  port: 8099  servlet:    context-path: /dynamic-datasourcespring:  application:    name: dynamic-datasource  datasource:    type: com.alibaba.druid.pool.DruidDataSource    druid:      master:        driver-class-name: com.mysql.cj.jdbc.Driver        url: jdbc:mysql://192.250.110.158:3306/gsfy_user?useUnicode=true&characterEncoding=utf8        username: root        password: admin@951753        initial-size: 5 # 初始化时建立物理连接的个数        min-idle: 20 # 最小连接池数量        max-active: 20 # 最大连接池数量      slave:        driver-class-name: com.mysql.cj.jdbc.Driver        url: jdbc:mysql://192.250.110.157:3306/level2?useUnicode=true&characterEncoding=utf8        username: dev        password: nfeb?4rfv        initial-size: 5 # 初始化时建立物理连接的个数        min-idle: 20 # 最小连接池数量        max-active: 20 # 最大连接池数量mybatis:  type-aliases-package: com.fullmoon.study.model  mapper-locations: classpath:mapper/*.xml  config-location: classpath:mybatis-config.xmlpagehelper:  helper-dialect: mysql  reasonable: true # 分页合理化参数  offset-as-page-num: true # 将 RowBounds 中的 offset 参数当成 pageNum 使用  supportMethodsArguments: true # 支持通过 Mapper 接口参数来传递分页参数

其中分别定义了 masterslave 数据源的相关配置

这样一来,在 DataSourceAspect 切面中根据自定义注解,设置 DataSourceContextHolder 当前线程所使用的数据源的 Key 值,MultipleDataSource 动态数据源则会根据该值设置需要使用的数据源,完成了动态数据源的切换

使用示例

在 Mapper 接口上面添加自定义注解 @TargetDataSource,如下:

@Repositorypublic interface UserMapper {    User queryUser(Integer id);    int insertUser(User user);    @TargetDataSource("slave")    Addr queryAddr(Integer id);    @TargetDataSource("slave")    int insertAddr(Addr addr);}

上面两个方法使用 master 数据源,下面两个方法使用 slave 数据源

总结

上面就如何配置动态数据源的实现方式仅提供一种思路,其中关于多事务方面并没有实现,采用 Spring 提供的事务管理器,如果同一个方法中使用了多个数据源,并不支持多事务的,需要自己去实现(笔者能力有限),可以整合JAT组件,参考:

分布式事务解决方案推荐使用

动态多数据源推荐使用

上一篇:精尽MyBatis源码分析 - 整体架构
下一篇:MyBatis 面试题

发表评论

最新留言

做的很好,不错不错
[***.243.131.199]2025年04月21日 04时23分57秒