mybatis plus自定义数据源
※星光※ 人气:6简介
基于springboot,mybatis plus集成了一套多数据源的解决方案,在使用时引入相应的插件dynamic-datasource-spring-boot-starter,可以实现数据源的动态添加、删除等功能,对于多租户或者分库等操作可以根据AOP切面代理到不同的数据源、实现单一系统数据隔离的目的。
代码示例
mavne依赖
<!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.4</version> </dependency> <!--dynamic-datasource--> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.4.1</version> </dependency>
数据源增加、移除
@RestController @RequestMapping("/datasources") public class DataSourceController { @Resource private DataSource dataSource; @Resource private DefaultDataSourceCreator dataSourceCreator; @GetMapping("list") public Set<String> list() { DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; return ds.getDataSources().keySet(); } @PostMapping("add") public Set<String> add(@Validated @RequestBody DataSourceDTO dto) { DataSourceProperty dataSourceProperty = new DataSourceProperty(); BeanUtils.copyProperties(dto, dataSourceProperty); DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty); ds.addDataSource(dto.getPollName(), dataSource); return ds.getDataSources().keySet(); } @DeleteMapping("remove") public void remove(String name) { DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource; ds.removeDataSource(name); } }
默认的数据源连接池加载顺序为: druid>hikaricp>beecp>dbcp>spring basic
数据源切换
基于AOP切换
添加注解,排除不做切换的接口
package com.starsray.dynamic.datasource.annotation; import java.lang.annotation.*; /** * <p> * 用户标识仅可以使用默认数据源 * </p> * * @author starsray * @since 2021-11-10 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DefaultDs { }
切面具体实现
package com.starsray.dynamic.datasource.interceptor; import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; import com.starsray.dynamic.datasource.annotation.DefaultDs; import com.starsray.dynamic.datasource.exception.ExceptionEnum; import com.starsray.dynamic.datasource.exception.GlobalException; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.AfterReturning; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; /** * <p> * 数据源选择器切面 * </p> * * @author starsray * @since 2021-11-10 */ @Aspect @Component @RequiredArgsConstructor(onConstructor_ = @Autowired) public class DsInterceptor implements HandlerInterceptor { @Pointcut("execution(public * com.starsray.dynamic.datasource.controller.*.*(..))") public void datasourcePointcut() { } /** * 前置操作,拦截具体请求,获取header里的数据源id,设置线程变量里,用于后续切换数据源 */ @Before("datasourcePointcut()") public void doBefore(JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); // 排除不可切换数据源的方法 DefaultDs annotation = method.getAnnotation(DefaultDs.class); if (null != annotation) { DynamicDataSourceContextHolder.push("master"); } else { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes; assert attributes != null; HttpServletRequest request = attributes.getRequest(); String header = request.getHeader("tenantName"); if (StringUtils.isNotBlank(header)) { DynamicDataSourceContextHolder.push(header); } else { throw new GlobalException(ExceptionEnum.NOT_TENANT); } } } /** * 后置操作,设置回默认的数据源id */ @AfterReturning("datasourcePointcut()") public void doAfter() { DynamicDataSourceContextHolder.push("master"); } }
基于重写处理器
mybatis plus提供了默认处理器来决定使用的数据源,可以重写处理器实现自定义参数,比如从请求header里面获取参数切换数据源。
@DS("#header.tenantId")
自定义处理器
public class HeaderProcessor extends DsProcessor { private static final String HEADER = "#header"; @Override public boolean matches(String key) { return key.startsWith(HEADER); } @Override public String doDetermineDatasource(MethodInvocation invocation, String key) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); return request.getHeader(key.substring(8)); } }
注册自定义处理器
@Configuration public class CustomerDynamicDataSourceConfig{ @Bean public DsProcessor dsProcessor() { DsHeaderProcessor headerProcessor = new DsHeaderProcessor(); DsSessionProcessor sessionProcessor = new DsSessionProcessor(); DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor(); headerProcessor.setNextProcessor(sessionProcessor); sessionProcessor.setNextProcessor(spelExpressionProcessor); return headerProcessor; } }
如果有场景需要手动切换数据源,可以使用组件提供的工具来实现。
DynamicDataSourceContextHolder.push("master");
自定义数据源
mybatis plus提供了一个接口来加载数据源信息。
public interface DynamicDataSourceProvider { Map<String, DataSource> loadDataSources(); }
这个接口有一个抽象实现类AbstractDataSourceProvider,通过模板方法定义了加载数据源来源的方式,mybatis plus通过YmlDynamicDataSourceProvider实现了读取yml文件配置来初始化数据源的方式。
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider { private static final Logger log = LoggerFactory.getLogger(AbstractDataSourceProvider.class); @Autowired private DefaultDataSourceCreator defaultDataSourceCreator; public AbstractDataSourceProvider() { } protected Map<String, DataSource> createDataSourceMap(Map<String, DataSourceProperty> dataSourcePropertiesMap) { Map<String, DataSource> dataSourceMap = new HashMap(dataSourcePropertiesMap.size() * 2); Iterator var3 = dataSourcePropertiesMap.entrySet().iterator(); while(var3.hasNext()) { Entry<String, DataSourceProperty> item = (Entry)var3.next(); String dsName = (String)item.getKey(); DataSourceProperty dataSourceProperty = (DataSourceProperty)item.getValue(); String poolName = dataSourceProperty.getPoolName(); if (poolName == null || "".equals(poolName)) { poolName = dsName; } dataSourceProperty.setPoolName(poolName); dataSourceMap.put(dsName, this.defaultDataSourceCreator.createDataSource(dataSourceProperty)); } return dataSourceMap; } }
如果有需要从数据库加载数据源信息,可以重写AbstractJdbcDataSourceProvider中的executeStmt方法来加载数据库配置信息。示例:
package com.digital.cnzz.dynamic.ds.provider; import com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider; import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider; import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; import com.digital.cnzz.dynamic.ds.config.DefaultDsConfig; import com.digital.cnzz.dynamic.ds.constant.DsDriverEnum; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.annotation.Resource; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.HashMap; import java.util.Map; @Primary @Configuration public class DsProvider { @Resource private DefaultDsConfig defaultDsConfig; @Bean public DynamicDataSourceProvider jdbcDynamicDataSourceProvider() { return new AbstractJdbcDataSourceProvider(defaultDsConfig.getDriverClassName(), defaultDsConfig.getUrl(), defaultDsConfig.getUsername(), defaultDsConfig.getPassword()) { @Override protected Map<String, DataSourceProperty> executeStmt(Statement statement) { Map<String, DataSourceProperty> dataSourcePropertiesMap = null; ResultSet rs = null; try { dataSourcePropertiesMap = new HashMap<>(); rs = statement.executeQuery("SELECT * FROM DYNAMIC_DATASOURCE_INSTANCE"); while (rs.next()) { String name = rs.getString("name"); DataSourceProperty property = new DataSourceProperty(); property.setDriverClassName(rs.getString("driver")); property.setUrl(rs.getString("url")); property.setUsername(rs.getString("username")); property.setPassword(rs.getString("password")); dataSourcePropertiesMap.put(name, property); } } catch (SQLException e) { e.printStackTrace(); } finally { try { if (rs != null) { rs.close(); } } catch (SQLException e) { e.printStackTrace(); } try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } return dataSourcePropertiesMap; } }; } }
通过读取源码可以发现,如果还有其他需要自定义加载数据源的方式,只需要继承AbstractDataSourceProvider抽象类,实现DynamicDataSourceProvider接口,重写loadDataSources方法就可以实现自定义数据源。
加载全部内容