mybaits+druid+aop 实现读写分离(支持已定义注解读数据切换主从库)

项目结构

             注释:通过druid+mybaits 实现读写分离,支持一主多送。支持自定义注解,实现部分从主库读取数据

mybaits+druid+aop 实现读写分离(支持已定义注解读数据切换主从库)

 

 

 

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

 

上一篇:一个HandlerMapping处理器适配器中都可以配置多个拦截器


下一篇:TestNG的代码组织层级