项目结构
注释:通过druid+mybaits 实现读写分离,支持一主多送。支持自定义注解,实现部分从主库读取数据
1.依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
2.yaml文件配置
spring: datasource: poolPreparedStatements: "true" druid: stat-view-servlet: login-password: "123" login-username: "admin" enabled: "true" useGlobalDataSourceStat: "true" slave2: url: "jdbc:mysql://localhost:3306/db03?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC" username: "root" type: "com.alibaba.druid.pool.DruidDataSource" master: type: "com.alibaba.druid.pool.DruidDataSource" url: "jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC" username: "root" slave1: username: "root" url: "jdbc:mysql://localhost:3306/db02?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC" type: "com.alibaba.druid.pool.DruidDataSource" maxWait: "60000" web-stat-filter: exclusions: "\"*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*\"" stat-view-servlet: url-pattern: "/druid/*" reset-enable: "false" validationQuery: "SELECT 1 FROM DUAL" minEvictableIdleTimeMillis: "300000" filters: "stat,wall,log4j" testOnBorrow: "false" testOnReturn: "false" initialSize: "5" maxActive: "20" maxPoolPreparedStatementPerConnectionSize: "20" testWhileIdle: "true" timeBetweenEvictionRunsMillis: "60000" minIdle: "5" url-pattern: "/*"
3.自定义注解master(用于有些读写都需要主数据库)
package com.hc.admin.annotation; import java.lang.annotation.*; /** * 自定义注解(用该注解标注的就读主库) * * @author summer.chou * @date 2021/01/21 * */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Master { }
4.DataSourceConfig
package com.hc.admin.dbconfig; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import lombok.extern.slf4j.Slf4j; 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; /** * mybatis+druid(实现读写分离) * 简单的读写分离并不使用这套架构,可以用nacos+seata 实现分表分库的读写分离) * * @author summmer.chou * @date 2021/01/21 */ @Configuration @Slf4j public class DataSourceConfig { /* * druid * */ @Bean @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.slave1") public DataSource slave1DataSource() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.slave2") public DataSource slave2DataSource() { return DruidDataSourceBuilder.create().build(); } @Bean public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slave1DataSource") DataSource slave1DataSource, @Qualifier("slave2DataSource") DataSource slave2DataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DBTypeEnum.MASTER, masterDataSource); targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource); targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource); log.info(targetDataSources.toString()); MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource(); myRoutingDataSource.setDefaultTargetDataSource(masterDataSource); myRoutingDataSource.setTargetDataSources(targetDataSources); return myRoutingDataSource; } }
5.DBContextHolder
package com.hc.admin.dbconfig; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.concurrent.atomic.AtomicInteger; /** * 获取当前线程所用数据类型 * * @author summer.chou * @date 2022/01/21 */ @Slf4j @Component public class DBContextHolder { /* * contextHolder 是线程变量,因为每个请求是一个线程,所以通过这样来区分使用哪个库 determineCurrentLookupKey是重写的AbstractRoutingDataSource的方法, 主要是确定当前应该使用哪个数据源的key,因为AbstractRoutingDataSource 中保存的多个数据源是通过Map的方式保存的 * */ private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>(); private static final AtomicInteger counter = new AtomicInteger(-1); //设置当前线程所用数据库类型 public static void set(DBTypeEnum dbType) { contextHolder.set(dbType); } //获取当前线程所用数据类型 public static DBTypeEnum get() { return contextHolder.get(); } public static void master() { set(DBTypeEnum.MASTER); log.info("切换到master"); } public static void slave() { // 轮询 int index = counter.getAndIncrement() % 2; if (counter.get() > 9999) { counter.set(-1); } if (index == 0) { set(DBTypeEnum.SLAVE1); log.info("切换到slave1"); } else { set(DBTypeEnum.SLAVE2); log.info("切换到slave2"); } } }
6.DBTypeEnum
package com.hc.admin.dbconfig; /** * 数据源枚举分类 * * @author summmer.chou * @date 2022/01/21 */ public enum DBTypeEnum { MASTER, SLAVE1, SLAVE2 }
7.MybatisConfig
package com.hc.admin.dbconfig; 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配置 * * @author summmer.chou * @date 2020/01/21 */ @EnableTransactionManagement @Configuration public class MybatisConfig { @Resource(name = "myRoutingDataSource") private DataSource myRoutingDataSource; /*由于Spring容器中现在有4个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。*/ @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(myRoutingDataSource); //我采取的是注解式sql,如果加上扫描,但包下无mapper.xml会报错 //sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); return sqlSessionFactoryBean.getObject(); } @Bean public PlatformTransactionManager platformTransactionManager() { return new DataSourceTransactionManager(myRoutingDataSource); } }
8.MyRoutingDataSource
package com.hc.admin.dbconfig; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.annotation.Nullable; /** * 获取路由key * * @author summmer.chou * @date 2022/01/21 */ public class MyRoutingDataSource extends AbstractRoutingDataSource { @Override @Nullable protected Object determineCurrentLookupKey() { return DBContextHolder.get(); } }
9.LogAndValidAop
package com.hc.admin.aspect; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.hc.admin.dbconfig.DBContextHolder; import org.apache.commons.lang.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Map; /** * @Description 1.数据库读写分离 * 2.自定义注解LogAnnotation+aop 打印请求路劲 +响应参数 (可截取注解参数,做全局日志持久化操作) * @Author summer.chou * @Date 2021/1/20 */ @Component @Aspect public class LogAndValidAop { private Logger logger = LoggerFactory.getLogger(LogAndValidAop.class); //切点强制 //主写从读,部分需要从主数据库读的使用自定义注解 @Pointcut("!@annotation(com.hc.admin.annotation.Master) " + "&& (execution(* com.hc.admin.service..*.select*(..)) " + "|| execution(* com.hc.admin.service..*.get*(..)))") public void readPointcut() { } //删除需要指定方法名开头的 @Pointcut("@annotation(com.hc.admin.annotation.Master) " + "|| execution(* com.hc.admin.service..*.insert*(..)) " + "|| execution(* com.hc.admin.service..*.add*(..)) " + "|| execution(* com.hc.admin.service..*.update*(..)) " + "|| execution(* com.hc.admin.service..*.edit*(..)) " + "|| execution(* com.hc.admin.service..*.delete*(..)) " + "|| execution(* com.hc.admin.service..*.remove*(..))") public void writePointcut() { } @Before("readPointcut()") public void read() { DBContextHolder.slave(); } @Before("writePointcut()") public void write() { DBContextHolder.master(); } //定义切点 @Pointcut("@annotation(com.hc.admin.annotation.LogAnnotation)") public void pointCutRestDef() { } //自定义打印日志 @Around("pointCutRestDef()") public Object processRst(ProceedingJoinPoint point) throws Throwable { Object returnValue = null; ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = sra.getRequest(); Object[] args = point.getArgs(); final List<Object> params = new ArrayList<>(); //获取非GET请求的参数 if (!"GET".equalsIgnoreCase(request.getMethod())) { for (int i = 0; i < args.length; i++) { Object object = args[i]; if (object instanceof HttpServletResponse) { continue; } if (object instanceof HttpServletRequest) { continue; } params.add(object); } } else { Enumeration names = request.getParameterNames(); JSONObject object = new JSONObject(); while (names.hasMoreElements()) { String name = (String) names.nextElement(); String value = request.getParameter(name); object.put(name, value); } params.add(object); } Long startTime = System.currentTimeMillis(); try { //获取返回值 returnValue = point.proceed(point.getArgs()); } catch (Exception e) { // 请求异常处理 throw e; } String url = request.getRequestURI();//获取接口路劲 String method = request.getMethod(); Long endTime = System.currentTimeMillis(); StringBuffer logString = new StringBuffer(); logString.append("\n" + " 请求方法 method :" + method); logString.append("\n" + " 响应时间 time:" + (endTime - startTime) + "ms"); logString.append("\n" + " 请求路径 url:" + url); logString.append("\n" + " 请求参数 param:" + JSONObject.toJSONString(params)); logString.append("\n" + " 返回结果 json:" + objTtoJson(returnValue)); logger.info(logString.toString()); //todo LogAnnotation获取注解参数值 用来拓展全局日志 // MethodSignature signature = (MethodSignature) point.getSignature(); //Method method = signature.getMethod(); // LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class); //添加日志 return returnValue; } public String objTtoJson(Object returnValue) { JSONObject object = new JSONObject(); JSONObject json = (JSONObject) JSON.toJSON(returnValue); for (Map.Entry<String, Object> entry : json.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); object.put(key, value); } return object.toJSONString(); } }