Java, 原创, 服务器, , ,

Spring boot 使用druid 多数据源 读写分离配置与调用 注解方式

分步骤,1配置多数据库,2.数据库调用,3.注解,4.AOP注入读写方式

目录结构如下, AOP的另一篇 注入 环绕通知 请求参数,返回结果统一处理

-aop
 --- MultiResponseAspect.java
-common
--- druid
----- DataSourceChoose.java
----- DataSourceGroup.java
----- DynamicDataSource.java
----- DynamicDataSourceHelper.java
----- SpringUtil.java
-config
--- DataSourceConfiguration.java

1.配置:

zeroDateTimeBehavior=CONVERT_TO_NULL 作用是日期为null的时候不报错自动转成 0000-00-00 00:00:00, type需要使用 alibaba的 DruidDataSource, 需要有 druid 的 key

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        pool-name: master
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test?zeroDateTimeBehavior=CONVERT_TO_NULL
        username: test
        password: test
        maximum-pool-size: 20
        minimun-idle: 5
      slave:
        pool-name: slave

        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test?zeroDateTimeBehavior=CONVERT_TO_NULL
        username: test
        password: test
        maximum-pool-size: 20
        minimun-idle: 5

2.创建相关类和配置

2.1 添加枚举类 DataSourceGroup.java,这里是 MASTER 和 SLAVE

package cn.test.fcs.common.druid;

/**
 * Create By test<chenxue4076@163.com>
 * File Name DataSourceGroup.java
 * Created Date 2020-12-10
 * Created Time 14:35
 */
public enum DataSourceGroup {
    //写
    MASTER,
    //读
    SLAVE
}

// end DataSourceGroup.java
//end file

2.2 添加动态加载数据源类 DynamicDataSource

package cn.test.fcs.common.druid;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.Map;

/**
 * Create By test<chenxue4076@163.com>
 * File Name DynamicDataSource.java
 * Created Date 2020-12-10
 * Created Time 14:50
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * targetDataSources:保存了key和数据库连接的映射关系
     * defaultTargetDataSource:表示默认的数据库连接
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources)    {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    @Override
    public Object determineCurrentLookupKey() {
        return DynamicDataSourceHelper.getDataSourceGroup();
    }
}
// end DynamicDataSource.java
//end file

2.3 添加动态设置和选择读写库的类,DynamicDataSourceHelper

package cn.test.fcs.common.druid;

/**
 * Create By test<chenxue4076@163.com>
 * File Name DynamicDataSourceHelper.java
 * Created Date 2020-12-10
 * Created Time 14:55
 */
public class DynamicDataSourceHelper {
    private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal<>();
    /**
     * 设置数据源的变量
     */
    public static void setDataSourceGroup(String dsGroup) {
        CONTEXT_HOLDER.set(dsGroup);
    }
    /**
     * 获得数据源的变量
     */
    public static String getDataSourceGroup() {
        return (String) CONTEXT_HOLDER.get();
    }
    /**
     * 清空数据源变量
     */
    public static void clearDataSourceGroup() {
        CONTEXT_HOLDER.remove();
    }
}
// end DynamicDataSourceHelper.java
//end file

2.4 添加一个获取Bean的类 SpringUtil, 可以根据字符串,class等获取

package cn.test.fcs.common.druid;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * Create By test<chenxue4076@163.com>
 * File Name SpringUtil.java
 * Created Date 2020-12-10
 * Created Time 15:07
 */
@Component
public class SpringUtil implements ApplicationContextAware {

    @Autowired
    private ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContextParam) throws BeansException {
        applicationContext = applicationContextParam;
    }
    public Object getObject(String id) {
        Object object = null;
        object = applicationContext.getBean(id);
        return object;
    }

    public <T> T getObject(Class<T> tClass) {
        return applicationContext.getBean(tClass);
    }

    public Object getBean(String tClass) {
        return applicationContext.getBean(tClass);
    }

    public <T> T getBean(String str,Class<T> tClass) {
        return (T)applicationContext.getBean(str);
    }

    public <T> T getBean(Class<T> tClass) {
        return applicationContext.getBean(tClass);
    }
}
// end SpringUtil.java
//end file

2.5 添加一个注解接口,用于执行SQL时选择读库还是写库

package cn.test.fcs.common.druid;

import java.lang.annotation.*;

/**
 * Create By test<chenxue4076@163.com>
 * File Name DataSourceChoose.java
 * Created Date 2020-12-10
 * Created Time 15:01
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSourceChoose {
    DataSourceGroup value() default DataSourceGroup.SLAVE;
}

// end DataSourceChoose.java
//end file

2.6 数据库配置与选择的类 DataSourceConfiguration 主角

package cn.test.fcs.config;

import cn.test.fcs.common.druid.SpringUtil;
import cn.test.fcs.common.druid.DataSourceGroup;
import cn.test.fcs.common.druid.DynamicDataSource;
import cn.test.fcs.common.druid.DynamicDataSourceHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Configuration
@ComponentScan(value = {"com.alibaba.druid.pool","DruidDataSource"})
public class DataSourceConfiguration {
    //private static Logger logger = LoggerFactory.getLogger(DataSourceConfiguration.class);
    @Autowired
    private SpringUtil springUtil;

    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;

    /**
     * master 数据源
     */
    @Bean(name = "masterDataSource", destroyMethod = "close", initMethod = "init")
    @ConfigurationProperties(prefix = "spring.datasource.druid.master")
    public DataSource masterDataSource() {
        log.info("-------------------Write Data Source Init----------------");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    /**
     * slave 数据源
     */
    @Bean(name = "slaveDataSource", destroyMethod = "close", initMethod = "init")
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource() {
        log.info("-------------------Read Data Source Init----------------");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource (DataSource slaveDataSource) {
        log.info("-------------------dynamicDataSource Init----------------");
        Map targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceGroup.SLAVE.name(), slaveDataSource);
        //设置备用
        setDataSource(targetDataSources, DataSourceGroup.MASTER.name(), "masterDataSource");
        //设置默认组
        DynamicDataSourceHelper.setDataSourceGroup(DataSourceGroup.SLAVE.name());

        DynamicDataSource dynamicDataSource = new DynamicDataSource(slaveDataSource, targetDataSources);
        log.info("------默认------"  + dynamicDataSource.determineCurrentLookupKey());
        return dynamicDataSource;
    }

    /**
     * 设置数据源
     * @param targetDataSources 备选数据源集合
     * @param sourceName 数据源名称
     * @param beanName bean名称
     */
    public void setDataSource(Map targetDataSources, String sourceName, String beanName) {
        try {
            //DataSource dataSource = (DataSource) SpringUtil.getBean(beanName);
            DataSource dataSource = (DataSource) springUtil.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        } catch (Exception e) {
            log.error(e.getMessage());
            e.printStackTrace();
        }
    }
    /**
     * 这里的list是多个从库的情况下为了实现简单负载均衡
     * @return
     * @throws SQLException
     */
    /*@Bean("readDataSources")
    public List<DataSource> readDataSources() throws SQLException {
        List<DataSource> dataSources=new ArrayList<>();
        dataSources.add(readDataSourceOne());
        return dataSources;
    }*/
}

3. 动态调用读写库,在mapper的方法中使用

package cn.test.fcs.crowdsource.gatewayimpl.database;

import cn.test.fcs.common.druid.DataSourceChoose;
import cn.test.fcs.common.druid.DataSourceGroup;
import cn.test.fcs.crowdsource.gatewayimpl.database.dataobject.SupplierDO;
import org.apache.ibatis.annotations.*;

import java.util.List;
import java.util.Map;

@Mapper
public interface CdSupplierMapper {
    String TABLE_NAME = "crowdsource_supplier";
    String ALL_COLUMNS = "id, supplier_name, supplier_short_name, contact_user, contact_mobile, contact_address, cooperate_start_date, " +
            "cooperate_end_date, note, insert_time, update_time, status, creator, modifier";

    @DataSourceChoose(value = DataSourceGroup.MASTER)
 //使用写库
    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
    @Insert("<script> " +
            "INSERT INTO " + TABLE_NAME + " (" + ALL_COLUMNS + ") VALUES ( null, #{supplierName}, #{supplierShortName}, #{contactUser}, #{contactMobile}, " +
            "#{contactAddress}, #{cooperateStartDate}, #{cooperateEndDate}, #{note}, now(), now(), 1, #{creator}, '') " +
            "</script>")
    int insertUseGeneratedKeys(SupplierDO supplierDO);

    @DataSourceChoose(value = DataSourceGroup.SLAVE)
 //使用读库或者不写
    @Select("SELECT " + ALL_COLUMNS + " FROM " + TABLE_NAME + " WHERE id = #{id} LIMIT 1")
    SupplierDO getById(@Param("id") int id);


    //不写使用哪种库,在AOP中设置默认为读库
    @MapKey("id")
    @Select("<script>" +
            "SELECT " + ALL_COLUMNS + " FROM " + TABLE_NAME + " WHERE id in " +
            "<if test='ids != null'>" +
            "   <foreach item='item' index='index' collection='ids' open='(' separator=',' close=')'>" +
            "       #{item} " +
            "   </foreach>" +
            "</if>" +
            "</script>")
    Map<Integer, SupplierDO> findByIds(@Param("ids") List<Integer> ids);
}

4. AOP 劫持 动态修改读写配置

package cn.test.fcs.aop;

import cn.test.fcs.common.Redis;
import cn.test.fcs.common.druid.DataSourceChoose;
import cn.test.fcs.common.druid.DataSourceGroup;
import cn.test.fcs.common.druid.DynamicDataSourceHelper;
import com.alibaba.cola.dto.MultiResponse;
import com.alibaba.cola.dto.Response;
import lombok.extern.slf4j.Slf4j;
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.aspectj.lang.reflect.MethodSignature;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;


import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * Create By test<chenxue4076@163.com>
 * File Name MultiResponseAspect.java
 * Created Date 2020/11/24
 * Created Time 15:52
 */
@Slf4j
@Component
@Aspect
public class MultiResponseAspect {
    //数据库环绕通知
    @Pointcut("execution(* cn.test.fcs.*.gatewayimpl.database.*Mapper.*(..))")
    public void aroundMapper() {}

    //数据库环绕通知
    @Around("aroundMapper()")
    public Object myAroundMapper(ProceedingJoinPoint proceedingJoinPoint) {
        //前置通知
        System.out.println("数据库前置通知在这里显示");
        //切换数据库
        DataSourceChoose dataSourceChoose = getDataSourceChoose(proceedingJoinPoint);
        if (dataSourceChoose != null) {
            DynamicDataSourceHelper.setDataSourceGroup(dataSourceChoose.value().name());
        } else {
        //设置默认为读库
            DynamicDataSourceHelper.setDataSourceGroup(DataSourceGroup.SLAVE.name());
        }
        //设置默认结果
        Object proceed = null;
        try{
            //方法执行
            System.out.println("数据库方法执行");
            log.info("数据库方法执行------使用数据库------"  + DynamicDataSourceHelper.getDataSourceGroup());
            proceed = proceedingJoinPoint.proceed();
        } catch (Throwable e) {
            log.error(e.getMessage());
            System.out.println(e.getMessage());
            return e;
        } finally {
            //最终通知
            System.out.println("数据库最终通知");
            DynamicDataSourceHelper.clearDataSourceGroup();
            return proceed;
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSourceChoose getDataSourceChoose(ProceedingJoinPoint proceedingJoinPoint) {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Class targetClass = proceedingJoinPoint.getTarget().getClass();
        //System.out.println("getDataSourceChoose targetClass " + targetClass.getName());
        DataSourceChoose targetDataSource = (DataSourceChoose)targetClass.getAnnotation(DataSourceChoose.class);
        //System.out.println("targetDataSource  DataSourceChoose " + targetDataSource);
        if (targetDataSource != null) {
            //System.out.println("targetDataSource has info " + targetDataSource.toString());
            return targetDataSource;
        } else {
            Method method = signature.getMethod();
            System.out.println("dataSourceChoose method " + method.getName());
            DataSourceChoose dataSourceChoose = method.getAnnotation(DataSourceChoose.class);
            System.out.println("dataSourceChoose info " + method.getName() + " " + dataSourceChoose);
            return dataSourceChoose;
        }
    }
}
// end MultiResponseAspect.java
//end file

正常来讲 通过上面几步就完成了读写分离的功能了。

(190)

Related Post