代码提交
This commit is contained in:
41
spring-data-jpa-read-write-separation/pom.xml
Normal file
41
spring-data-jpa-read-write-separation/pom.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.mangmang</groupId>
|
||||
<artifactId>learning-nexus</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-data-jpa-read-write-separation</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.mangmang;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
|
||||
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
|
||||
public class SpringDataJpaReadWriteSeparationApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SpringDataJpaReadWriteSeparationApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.mangmang.annotaion;
|
||||
|
||||
public enum DataSource {
|
||||
MASTER, SLAVE;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.mangmang.annotaion;
|
||||
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据源切换注解,用于在方法或类级别指定使用的数据源
|
||||
* 该注解可应用于类或方法上,运行时生效,并会保留在javadoc文档中
|
||||
* @see DataSource 数据源类型枚举,包含可用数据源定义(如MASTER主库、SLAVE从库等)
|
||||
* 参数说明:
|
||||
* @value 指定目标数据源名称,默认使用MASTER主库数据源
|
||||
* 通过该参数实现数据源动态切换,需配合AOP或拦截器实现具体切换逻辑
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface TargetDataSource {
|
||||
/**
|
||||
* 数据源类型配置参数
|
||||
* @return 数据源枚举值,默认返回主库数据源标识
|
||||
*/
|
||||
DataSource value() default DataSource.MASTER;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.mangmang.config;
|
||||
|
||||
import com.mangmang.annotaion.DataSource;
|
||||
import com.mangmang.annotaion.TargetDataSource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* 动态数据源切面类,用于实现基于注解的数据源动态切换功能
|
||||
*
|
||||
* 本切面通过拦截带有@TargetDataSource注解的方法,在执行目标方法前切换数据源,
|
||||
* 执行完成后自动清理数据源上下文,实现主从库的动态路由
|
||||
*/
|
||||
@Slf4j
|
||||
@Order(1)
|
||||
@Aspect
|
||||
@Component
|
||||
public class DataSourceAspect {
|
||||
|
||||
/**
|
||||
* 定义切点:拦截所有标注@TargetDataSource注解的方法
|
||||
*
|
||||
* @Pointcut 使用注解表达式定位需要拦截的方法,注解全路径为com.mangmang.annotaion.TargetDataSource
|
||||
*/
|
||||
@Pointcut("@annotation(com.mangmang.annotaion.TargetDataSource)")
|
||||
public void dataSourcePointcut() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 环绕通知方法,实现数据源的动态切换和清理
|
||||
*
|
||||
* @param point 连接点对象,提供被拦截方法的相关信息
|
||||
* @return Object 目标方法的执行结果
|
||||
* @throws Throwable 目标方法可能抛出的异常
|
||||
*
|
||||
* 执行流程:
|
||||
* 1. 获取方法签名和注解信息
|
||||
* 2. 根据注解值设置数据源(无注解时默认主库)
|
||||
* 3. 执行目标方法
|
||||
* 4. 清理线程数据源上下文
|
||||
*/
|
||||
@Around("dataSourcePointcut()")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
// 获取方法元数据
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
// 解析目标数据源注解
|
||||
TargetDataSource ds = method.getAnnotation(TargetDataSource.class);
|
||||
|
||||
// 设置数据源策略:有注解使用注解值,无注解使用默认主库
|
||||
if (ds == null) {
|
||||
DynamicDataSource.DynamicDataSourceContextHolder.setDataSource(DataSource.MASTER.name());
|
||||
log.info("使用主库");
|
||||
} else {
|
||||
DynamicDataSource.DynamicDataSourceContextHolder.setDataSource(ds.value().name());
|
||||
}
|
||||
|
||||
try {
|
||||
// 执行被拦截的目标方法
|
||||
return point.proceed();
|
||||
} finally {
|
||||
// 确保线程数据源上下文清理,避免内存泄漏
|
||||
DynamicDataSource.DynamicDataSourceContextHolder.clearDataSource();
|
||||
log.info("清除数据源");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.mangmang.config;
|
||||
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
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 javax.sql.DataSource;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据源配置类,用于配置主从数据源和动态路由数据源
|
||||
*/
|
||||
@Configuration
|
||||
public class DataSourceConfig {
|
||||
|
||||
/**
|
||||
* 创建主数据源Bean
|
||||
* @return 配置好的主数据源实例,自动绑定spring.datasource.master前缀的配置属性
|
||||
*/
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "spring.datasource.master")
|
||||
public DataSource masterDataSource() {
|
||||
return DataSourceBuilder.create().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建从数据源Bean
|
||||
* @return 配置好的从数据源实例,自动绑定spring.datasource.slave前缀的配置属性
|
||||
*/
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "spring.datasource.slave")
|
||||
public DataSource slaveDataSource() {
|
||||
return DataSourceBuilder.create().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动态数据源路由(主数据源作为默认数据源)
|
||||
* @param masterDataSource 通过@Qualifier注入的主数据源实例
|
||||
* @param slaveDataSource 通过@Qualifier注入的从数据源实例
|
||||
* @return 配置好的动态数据源路由实例
|
||||
*/
|
||||
@Bean
|
||||
@Primary
|
||||
public DynamicDataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
|
||||
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
|
||||
DynamicDataSource dynamicDataSource = new DynamicDataSource();
|
||||
// 构建数据源映射表
|
||||
Map<Object, Object> dataSourceMap = new HashMap<>(2);
|
||||
dataSourceMap.put(com.mangmang.annotaion.DataSource.MASTER.name(), masterDataSource);
|
||||
dataSourceMap.put(com.mangmang.annotaion.DataSource.SLAVE.name(), slaveDataSource);
|
||||
|
||||
// 设置默认数据源和完整数据源集合
|
||||
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
|
||||
dynamicDataSource.setTargetDataSources(dataSourceMap);
|
||||
|
||||
return dynamicDataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建事务管理器
|
||||
* @param dynamicDataSource 动态数据源路由实例
|
||||
* @return 配置好的事务管理器实例
|
||||
*/
|
||||
@Bean
|
||||
public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
|
||||
return new DataSourceTransactionManager(dynamicDataSource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.mangmang.config;
|
||||
|
||||
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
|
||||
|
||||
|
||||
/**
|
||||
* 动态数据源路由实现类,继承Spring框架的AbstractRoutingDataSource
|
||||
* 通过重写determineCurrentLookupKey方法实现多数据源动态切换
|
||||
*/
|
||||
public class DynamicDataSource extends AbstractRoutingDataSource {
|
||||
/**
|
||||
* 获取当前线程绑定的数据源标识
|
||||
* @return String 数据源标识key,对应配置的数据源映射
|
||||
*/
|
||||
@Override
|
||||
protected Object determineCurrentLookupKey() {
|
||||
return DynamicDataSourceContextHolder.getDataSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据源上下文持有器(线程安全)
|
||||
* 使用ThreadLocal实现线程级数据源隔离,通过set/get/clear方法管理当前线程数据源标识
|
||||
*/
|
||||
public static class DynamicDataSourceContextHolder {
|
||||
// 线程本地变量存储数据源标识
|
||||
private final static ThreadLocal<String> contextHolder = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 绑定数据源标识到当前线程
|
||||
* @param dataSource 数据源标识key
|
||||
*/
|
||||
public static void setDataSource(String dataSource) {
|
||||
contextHolder.set(dataSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前线程绑定的数据源标识
|
||||
* @return String 当前数据源标识,可能为null
|
||||
*/
|
||||
public static String getDataSource() {
|
||||
return contextHolder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前线程的数据源绑定
|
||||
*/
|
||||
public static void clearDataSource() {
|
||||
contextHolder.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.mangmang.controller;
|
||||
|
||||
import com.mangmang.entity.User;
|
||||
import com.mangmang.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> createUser(@RequestBody User user) {
|
||||
return ResponseEntity.ok(userService.saveUser(user));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUserById(@PathVariable Long id) {
|
||||
return userService.findById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<User>> getAllUsers() {
|
||||
return ResponseEntity.ok(userService.findAllUsers());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.mangmang.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Integer id;
|
||||
|
||||
private String username;
|
||||
|
||||
private String email;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.mangmang.repository;
|
||||
|
||||
import com.mangmang.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.mangmang.service;
|
||||
|
||||
import com.mangmang.entity.User;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserService {
|
||||
|
||||
User saveUser(User user);
|
||||
|
||||
Optional<User> findById(Long id);
|
||||
|
||||
List<User> findAllUsers();
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.mangmang.service.impl;
|
||||
|
||||
import com.mangmang.annotaion.DataSource;
|
||||
import com.mangmang.annotaion.TargetDataSource;
|
||||
import com.mangmang.entity.User;
|
||||
import com.mangmang.repository.UserRepository;
|
||||
import com.mangmang.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
public UserServiceImpl(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public User saveUser(User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@TargetDataSource(DataSource.SLAVE)
|
||||
@Override
|
||||
public Optional<User> findById(Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@TargetDataSource(DataSource.SLAVE)
|
||||
@Override
|
||||
public List<User> findAllUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
spring:
|
||||
datasource:
|
||||
master:
|
||||
jdbc-url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
slave:
|
||||
jdbc-url: jdbc:mysql://localhost:3307/slave_db?useSSL=false&serverTimezone=UTC
|
||||
username: root
|
||||
password: root
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
jpa:
|
||||
show-sql: true
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
|
||||
Reference in New Issue
Block a user