代码提交

This commit is contained in:
liujing33
2025-05-08 18:02:47 +08:00
parent 55fcc338d7
commit ba04a1047b
60 changed files with 1961 additions and 201 deletions

View 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>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,5 @@
package com.mangmang.annotaion;
public enum DataSource {
MASTER, SLAVE;
}

View File

@@ -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;
}

View File

@@ -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("清除数据源");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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> {
}

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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