目的
一般公司的数据访问层有多种方式,比如说mybaitis和springjdbc。此外,还可能会使用sharding-jdbc来实现分库分表,并使用druid提供的datasource。一个合格的数据访问层应该是把他们结合起来。还可以有一些其他的功能,比如说实现慢sql的监控,缓存(当然mybaitis自己有本地缓存,这里指的是外部缓存),事务等等。
定义你的注解
注解是用来给开发者使用的。你的数据库访问层唯一对开发者java代码的侵入就应该是注解。注解用来配置数据源名,分库分表方式等等。
@Target : 用来说明该注解可以被声明在那些元素之前。这里使用ElementType.TYPE:说明该注解只能被声明在一个类前。
@Retention :用来说明该注解类的生命周期。RetentionPolicy.RUNTIME : 注解保留在程序运行期间,此时可以通过反射获得定义在某个类上的所有注解。
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。 如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
@Documented 注解表明这个注解应该被 javadoc工具记录. 默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理, 所以注解类型信息也会被包括在生成的文档中
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DataSource {
/**
* 数据源名,缺省为初始数据源
*/
String value() default "DEFAULT";
}
当然,为了实现分库分表,你还可以定义自己的sharding注解
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface TableSharding {
/**
* 表名
*/
public String tableName();
/**
* 分表方式
*/
public String shardingType();
/**
* 根据什么字段分表
*/
public String shardingColumn();
public String locationRange();
public String mapperClassReference();
}
初始化
我们知道,Spring中实现BeanFactoryPostProcessor接口的类在容器实例化任何bean之前读取bean的定义(配置元数据),并可以修改它。而BeanDefinitionRegistryPostProcessor调用发生在容器加载完所有bean定义后,在BeanFactoryPostProcessor之前,区别是在这里你可以新增bean。在这里,我们完成扫描工程中所有mapper文件。事实上,这也是是mybaits和spring整合的原理:即通过实现BeanDefinitionRegistryPostProcessor来扫描工程中的mapper文件来注册数据访问bean。(当然,你也可以一个个写xml配置)。
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
try {
//MapperScannerConfigurer类是mybaits提供的扫描mapper文件的类,此处的MyMapperScannerConfigurerz做必要的功能扩充。
MyMapperScannerConfigurer mapperScannerConfigurer = new MyMapperScannerConfigurer();
String basepackge = this.getBasePackage();
mapperScannerConfigurer.setBasePackage(basepackge);
String mapperLocation = this.getMapperLocations();
if (StringUtils.isNotBlank(mapperLocation)) {
Resource [] mapperResource = new Resource[1];
ResourcePatternResolver resourceLoader = new PathMatchingResourcePatternResolver();
try {
Resource[] r = resourceLoader.getResources(mapperLocation);
mapperResource = (Resource [])ArrayUtils.addAll(mapperResource, r);
mapperScannerConfigurer.setMapperLocations(mapperResource);
} catch (Exception e) {
System.out.println("resolve mapper location error: " + e);
}
}
//dataSource是根据xml配置文件注入的一个multidatasource对象,代表多种可能的对象。 mapperScannerConfigurer.setDataSource(dataSource);
Configuration configuration = createConfigurationByXml();
mapperScannerConfigurer.setConfiguration(configuration);
mapperScannerConfigurer.setTypeAliasesPackage(typeAliasesPackage);
//必须得加上这一行
mapperScannerConfigurer.setApplicationContext(this.applicationContext);
mapperScannerConfigurer.afterPropertiesSet();
mapperScannerConfigurer.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
registerDALClient();
} catch (Exception e) {
e.printStackTrace();
}
}
我们的第二步初始化发生在BeanPostProcessor.postProcessBeforeInitialization中。这个方法在任何实例的初始化之前进行。这个主要是对spring jdbc中的JdbcDaoSupport中的setDataSource进行注入(@datasource)。同时,也顺便检查下mybaits的@datasource标记的数据源的合法性。
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class clazz = bean.getClass();
while (clazz != Object.class) {
if (clazz == MapperFactoryBean.class) {
MapperFactoryBean factoryBean = (MapperFactoryBean) bean;
Class actualMapper = factoryBean.getMapperInterface();
DataSource dataSource = (DataSource)actualMapper.getAnnotation(DataSource.class);
TableShardingRuler tableShardingRuler = (TableShardingRuler)actualMapper.getAnnotation(TableShardingRuler.class);
//前后两个mapper的sqlSessionFactory可能指向同一个地址,
// Environment environment = factoryBean.getSqlSession().getConfiguration().getEnvironment();
javax.sql.DataSource db = getDataSourceForThisDao(actualMapper, beanName, dataSource, tableShardingRuler);
// Environment environment_new = new Environment(environment.getId(), environment.getTransactionFactory(), db);
// factoryBean.getSqlSession().getConfiguration().setEnvironment(environment_new);
break;
} else if (clazz == JdbcDaoSupport.class) {
try {
DataSource dataSource = bean.getClass().getAnnotation(DataSource.class);
TableShardingRuler tableShardingRuler = bean.getClass().getAnnotation(TableShardingRuler.class);
javax.sql.DataSource db = getDataSourceForThisDao(bean, beanName, dataSource, tableShardingRuler);
Method m = clazz.getDeclaredMethod("setDataSource", javax.sql.DataSource.class);
m.invoke(bean, db);
break;
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
e.printStackTrace();
break;
}
} else {
clazz = clazz.getSuperclass();
}
}
return bean;
}
拦截器(mybaitis)
我们进行基于mybaits的拦截,来拦截mybaitis的数据源请求。mybaits的@interceptor可以进行拦截。(注意,springjdbc并不需要这一步)
@Intercepts({@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
), @Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class DataSourceInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
DataSource dataSource = (DataSource)Class.forName(className).getAnnotation(DataSource.class);
MultiDataSourcesSwitcher.setDataSourceType(MultiDataSources.getDefaultDBType());
}
}
拦截的主要目的是根据注解的值选定具体的数据源。数据源被保存在一个ThreadLocal变量中。
实际干活
是利用spring jdbc的AbstractRoutingDataSource支持多个数据源。在getconnection时,会根据上面的ThreadLocal选择具体的数据源。
配置cat监控
利用druid提供的StatFilter可以提供你对sql运行事件的一些监控。你可以继承该类。
然后在spring配置文件中作如下配置
<bean id="myStatFilterAspect" class="com.umetrip.dal.druid.MyStatFilterAspect" />