springboot多数据源动态切换和自定义mybatis分页插件
1、配置多数据源
增加druid依赖
完整pom文件
数据源配置文件
route.datasource.driver-class-name= com.mysql.jdbc.Driver route.datasource.url= jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8 route.datasource.username= root route.datasource.password= 123456 operate.datasource.driver-class-name= com.mysql.jdbc.Driver operate.datasource.url= jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8 operate.datasource.username= root operate.datasource.password= 123456
初始化数据源
/** * 多数据源配置 * @author zhouliang * @date 2017年9月20日 */ @Configuration @PropertySource("classpath:multiple-datasource.properties") public class MyBatisConfig { @Autowired private Environment env; @Autowired MybatisSpringPageInterceptor inteceptor; /** * 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称) */ @Bean public DataSource operateDataSource() throws Exception { Properties props = new Properties(); props.put("driverClassName", env.getProperty("operate.datasource.driver-class-name")); props.put("url", env.getProperty("operate.datasource.url")); props.put("username", env.getProperty("operate.datasource.username")); props.put("password", env.getProperty("operate.datasource.password")); return DruidDataSourceFactory.createDataSource(props); } @Bean public DataSource routeDataSource() throws Exception { Properties props = new Properties(); props.put("driverClassName", env.getProperty("route.datasource.driver-class-name")); props.put("url", env.getProperty("route.datasource.url")); props.put("username", env.getProperty("route.datasource.username")); props.put("password", env.getProperty("route.datasource.password")); return DruidDataSourceFactory.createDataSource(props); } /** * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错 * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例) */ @Bean @Primary public DynamicDataSource dataSource( @Qualifier("routeDataSource") DataSource routeDataSource, @Qualifier("operateDataSource") DataSource operateDataSource) { Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); targetDataSources.put(DatabaseType.routeDS, routeDataSource); targetDataSources.put(DatabaseType.operateDS, operateDataSource); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法 return dataSource; } /** * 根据数据源创建SqlSessionFactory */ @Bean public SqlSessionFactory sqlSessionFactory(DynamicDataSource ds) throws Exception { SqlSessionFactoryBean fb = new SqlSessionFactoryBean(); fb.setDataSource(ds);// 指定数据源(这个必须有,否则报错) // 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加 fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包 fb.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(env.getProperty("mybatis.mapperLocations")));// fb.setPlugins(new Interceptor[]{inteceptor}); return fb.getObject(); } }
@Bean是分别注入两个数据源,
当自动注入多个同样的bean时需要指定一个默认额,所以这里指定了一个默认的抽象数据源@primary
数据源注入之后需要创建SqlSessionFactory
创建抽象数据源类集成AbstractRoutingDataSource
1 public class DynamicDataSource extends AbstractRoutingDataSource { 2 3 4 @Override 5 protected Object determineCurrentLookupKey() { 6 // TODO Auto-generated method stub 7 return DatabaseContextHolder.getDatabaseType(); 8 } 9 10 }
1 public enum DatabaseType { 2 routeDS,operateDS 3 }
1 public class DatabaseContextHolder { 2 private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<DatabaseType>(); 3 4 public static void setDatabaseType(DatabaseType type){ 5 contextHolder.set(type); 6 } 7 8 public static DatabaseType getDatabaseType(){ 9 return contextHolder.get(); 10 } 11 public static void clearDatabaseType(){ 12 contextHolder.remove(); 13 }
上述配置完成之后需要在配置文件中指定mybatis映射的xml文件的位置
指定mybatis映射文件之后需要在项目的启动类上排除springboot默认的数据库的配置以及指定mybatis映射文件对应的接口
不排除springboot默认的数据库配置类,项目启动的时候会报错启动失败
@MapperScan指定了mybatis映射文件对应的接口所在的目录,这样避免了再每个接口上都加上@Mapper的注解
到此两个数据源已经配置完成
2、数据源的动态切换
这里设置了所有的实体bean都继承了一个父类,父类信息如下
1 public class BaseBean implements Serializable{ 2 private int pageSize=10; 3 private int pageNo=0; 4 private long totalNum; 5 private String totalMappedStatementId; 6 7 8 private long shardValue = 0l; 9 10 public BaseBean() { 11 super(); 12 // TODO Auto-generated constructor stub 13 } 14 15 public BaseBean(int pageSize, int pageNo, int totalNum) { 16 super(); 17 this.pageSize = pageSize; 18 this.pageNo = pageNo; 19 this.totalNum = totalNum; 20 } 21 22 public int getPageSize() { 23 return pageSize; 24 } 25 26 public long getShardValue() { 27 return shardValue; 28 } 29 30 public void setShardValue(long shardValue) { 31 this.shardValue = shardValue; 32 } 33 34 public String getTotalMappedStatementId() { 35 return totalMappedStatementId; 36 } 37 38 public void setTotalMappedStatementId(String totalMappedStatementId) { 39 this.totalMappedStatementId = totalMappedStatementId; 40 } 41 42 public void setPageSize(int pageSize) { 43 this.pageSize = pageSize; 44 } 45 46 public int getPageNo() { 47 return pageNo; 48 } 49 50 public void setPageNo(int pageNo) { 51 this.pageNo = pageNo; 52 } 53 54 public long getTotalNum() { 55 return totalNum; 56 } 57 58 public void setTotalNum(long totalNum) { 59 this.totalNum = totalNum; 60 } 61 62 }
BaseBean中的shardValue属性是用来指定数据源的,默认值为0,其余属性是分页相关的。
数据源的动态切换是通过spring的切面编程来实现的,通过对mybatis的映射文件对应的接口进行监控,代码如下
1 @Aspect 2 @Component 3 public class DataSourceAspect { 4 Logger logger = LoggerFactory.getLogger(DataSourceAspect.class); 5 6 @Before("execution(* zl.mybatis.mapper.*.*(..))") 7 public void setDataSourcePgKey(JoinPoint point) { 8 Object args[] = point.getArgs(); 9 for(Object obj:args){ 10 if(obj instanceof BaseBean){ 11 BaseBean bean = (BaseBean) obj; 12 if(Common.DB_0==bean.getShardValue()){ 13 logger.info("===========================使用数据源DB_route======================="); 14 DatabaseContextHolder.setDatabaseType(DatabaseType.routeDS); 15 }else{ 16 logger.info("===========================使用数据源DB_operate======================="); 17 DatabaseContextHolder.setDatabaseType(DatabaseType.operateDS); 18 } 19 break; 20 } 21 } 22 } 23 24 }
@Before("execution(* zl.mybatis.mapper.*.*(..))")是对mybatis的映射文件对应的接口进行监控,根据获取到的参数实体类判断里面的shardValue的值来决定使用哪个数据源
完成这一步之后springboot的多数据源动态切换完成了,接下来继续
3、实现自定义mybatis的分页插件
mybatis本身提供了对数据库操作的拦截器,所以实现自定义分页的时候只需要实现这个接口自定义里面的拦截方法。这里我是只拦截了查询的方法
具体代码如下
1 @Component 2 @Intercepts({ @Signature(type = Executor.class, method = "query", 3 args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) }) 4 public class MybatisSpringPageInterceptor implements Interceptor { 5 6 private static final Logger logger = LoggerFactory.getLogger(MybatisSpringPageInterceptor.class.getName()); 7 8 9 @SuppressWarnings("unused") 10 public Object intercept(Invocation arg0) throws Throwable { 11 MappedStatement mappedStatement = (MappedStatement) arg0.getArgs()[0]; 12 Object parameter = arg0.getArgs()[1]; 13 BoundSql boundSql = mappedStatement.getBoundSql(parameter); 14 if (null == boundSql || StringUtils.isBlank(boundSql.getSql())) { 15 return null; 16 } 17 RowBounds rowBounds = (RowBounds) arg0.getArgs()[2]; 18 Object parameterObject = boundSql.getParameterObject(); 19 BaseBean model = null; 20 if (parameterObject instanceof BaseBean) { 21 model = (BaseBean) parameterObject; 22 } else { 23 BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, boundSql.getSql()); 24 arg0.getArgs()[0] = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql)); 25 return arg0.proceed(); 26 } 27 if (null == model) { 28 throw new Exception("无法获取分页参数."); 29 } 30 if (model.getPageNo() == -1) { 31 BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, boundSql.getSql()); 32 arg0.getArgs()[0] = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql)); 33 return arg0.proceed(); 34 } 35 String shardSql = boundSql.getSql(); 36 queryTotal(mappedStatement, shardSql, parameterObject, boundSql,model); 37 38 if (null == rowBounds || rowBounds == RowBounds.DEFAULT) { 39 rowBounds = new RowBounds(model.getPageSize() * (model.getPageNo() - 1), model.getPageSize()); 40 } 41 String pagesql = getLimitSql(shardSql, rowBounds.getOffset(), rowBounds.getLimit()); 42 arg0.getArgs()[2] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT); 43 BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, pagesql); 44 arg0.getArgs()[0] = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql)); 45 return arg0.proceed(); 46 } 47 48 public static class BoundSqlSqlSource implements SqlSource { 49 BoundSql boundSql; 50 51 public BoundSqlSqlSource(BoundSql boundSql) { 52 this.boundSql = boundSql; 53 } 54 55 public BoundSql getBoundSql(Object parameterObject) { 56 return boundSql; 57 } 58 } 59 private String getLimitSql(String sql, int start, int end) throws Exception{ 60 if(sql ==null){ 61 throw new Exception("execute sql is empty."); 62 } 63 StringBuffer sqlBuffer = new StringBuffer(sql.length()+300); 64 sqlBuffer.append(sql); 65 sqlBuffer.append(" LIMIT ").append(start).append(",").append(end); 66 return sqlBuffer.toString(); 67 } 68 private void queryTotal(MappedStatement mappedStatement, String replaceSql, Object parameterObject, BoundSql boundSql,BaseBean model) throws Exception{ 69 StringBuffer countSql = new StringBuffer(); 70 71 if(model.getTotalMappedStatementId()!=null && model.getTotalMappedStatementId().length()>0){ 72 MappedStatement totalMappedStatement=mappedStatement.getConfiguration().getMappedStatement(model.getTotalMappedStatementId()); 73 BoundSql totalBoundSql = totalMappedStatement.getBoundSql(parameterObject); 74 75 countSql.append(totalBoundSql.getSql()); 76 }else{ 77 // 未指定,自动拼装 78 countSql.append("SELECT COUNT(1) FROM (").append(replaceSql).append(") as total"); 79 } 80 81 Connection conn = null; 82 PreparedStatement ps = null; 83 ResultSet rs = null; 84 try { 85 conn = mappedStatement.getConfiguration().getEnvironment().getDataSource().getConnection(); 86 if (logger.isDebugEnabled()) { 87 logger.debug(countSql.toString()); 88 } 89 ps = conn.prepareStatement(countSql.toString()); 90 BoundSql countBS = copyFromBoundSql(mappedStatement, boundSql, countSql.toString()); 91 setParameters(ps, mappedStatement, countBS, parameterObject); 92 rs = ps.executeQuery(); 93 if (rs.next()) { 94 model.setTotalNum(rs.getLong(1)); 95 } 96 } catch (Exception e) { 97 logger.error(e.getMessage(), e); 98 throw new Exception(e.getMessage(), e); 99 } finally { 100 try { 101 if (null != rs) { 102 rs.close(); 103 } 104 } catch (Exception e) { 105 logger.error("rs.close() error!", e); 106 } 107 try { 108 if (null != ps) { 109 ps.close(); 110 } 111 } catch (Exception e) { 112 logger.error("ps.close() error!", e); 113 } 114 try { 115 if (null != conn) { 116 conn.close(); 117 } 118 } catch (Exception e) { 119 logger.error("conn.close() error!", e); 120 } 121 } 122 } 123 protected MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) { 124 Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType()); 125 builder.resource(ms.getResource()); 126 builder.fetchSize(ms.getFetchSize()); 127 builder.statementType(ms.getStatementType()); 128 builder.keyGenerator(ms.getKeyGenerator()); 129 // builder.keyProperty(ms.getKeyProperties()); 130 builder.timeout(ms.getTimeout()); 131 builder.parameterMap(ms.getParameterMap()); 132 builder.resultMaps(ms.getResultMaps()); 133 builder.cache(ms.getCache()); 134 MappedStatement newMs = builder.build(); 135 return newMs; 136 } 137 138 /** 139 * 140 * @param ps 141 * @param mappedStatement 142 * @param boundSql 143 * @param parameterObject 144 * @throws SQLException 145 */ 146 private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException { 147 ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); 148 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); 149 if (parameterMappings != null) { 150 Configuration configuration = mappedStatement.getConfiguration(); 151 TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); 152 MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject); 153 for (int i = 0; i < parameterMappings.size(); i++) { 154 ParameterMapping parameterMapping = parameterMappings.get(i); 155 if (parameterMapping.getMode() != ParameterMode.OUT) { 156 Object value; 157 String propertyName = parameterMapping.getProperty(); 158 if (parameterObject == null) { 159 value = null; 160 } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { 161 value = parameterObject; 162 } else if (boundSql.hasAdditionalParameter(propertyName)) { 163 value = boundSql.getAdditionalParameter(propertyName); 164 } else { 165 value = metaObject == null ? null : metaObject.getValue(propertyName); 166 } 167 TypeHandler typeHandler = parameterMapping.getTypeHandler(); 168 if (typeHandler == null) { 169 throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId()); 170 } 171 logger.debug(i + 1 + ":" + value); 172 typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType()); 173 } 174 } 175 } 176 } 177 @Override 178 public Object plugin(Object arg0) { 179 return Plugin.wrap(arg0, this); 180 } 181 182 public void setProperties(Properties arg0) { 183 184 } 185 186 187 private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql, String sql) { 188 BoundSql newBoundSql = new BoundSql(ms.getConfiguration(),sql, boundSql.getParameterMappings(), boundSql.getParameterObject()); 189 for (ParameterMapping mapping : boundSql.getParameterMappings()) { 190 String prop = mapping.getProperty(); 191 if (boundSql.hasAdditionalParameter(prop)) { 192 newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop)); 193 } 194 } 195 return newBoundSql; 196 } 197 198 }
插件重写完成之后需要在注入SqlSessionFactory的时候指定这个插件,下面的代码是在MyBatisConfig.java中
现在已经完成了多数据源的动态切换以及自定义mybatis的分页查询了,剩下的测试步骤就不再啰嗦了,排版比较乱大家就将就着看吧!