目的
一般公司的数据访问层有多种方式,比如说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();
}
初始化
Mybiats初始化(注册Mapper为数据访问对象)
我们知道,Spring中实现BeanFactoryPostProcessor接口的类在容器实例化任何bean之前读取bean的定义(配置元数据),并可以修改它。
BeanDefinitionRegistryPostProcessor调用发生在容器加载完所有bean定义后,在BeanFactoryPostProcessor之前,区别是在这里你可以新增bean。在这里,我们完成扫描工程中所有mapper文件。事实上,这也是是mybaits和spring整合的原理:即通过实现BeanDefinitionRegistryPostProcessor来扫描工程中的mapper文件来注册数据访问bean。(当然,你也可以一个个写xml配置)。这里封装了Mybiats提供的mybaits-spring的实现。主要是用到了MapperScannerConfigurer类(扫描Mapper转化为bean)和SqlSessionFactory(主要用于保存Mybatis的相关设置和datasource)
关于其具体使用请参考
http://www.mybatis.org/spring/zh/getting-started.html
public class GdlMapperScannerFactory implements ApplicationContextAware, BeanDefinitionRegistryPostProcessor {
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
try {
//MapperScannerConfigurer类是mybaits提供的扫描mapper文件的类,此处的MyMapperScannerConfigurer做必要的功能扩充。
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);
//读取mybaits自己的配置文件
Configuration configuration = createConfigurationByXml();
mapperScannerConfigurer.setConfiguration(configuration);
mapperScannerConfigurer.setTypeAliasesPackage(typeAliasesPackage);
mapperScannerConfigurer.setApplicationContext(this.applicationContext);
mapperScannerConfigurer.afterPropertiesSet();
//完成在容器中注册Mapper转化来的dao对象
mapperScannerConfigurer.postProcessBeanDefinitionRegistry(beanDefinitionRegistry);
//在Zookeepr上注册本客户端
registerDALClient();
} catch (Exception e) {
e.printStackTrace();
}
}
spring-jdbc初始化
我们的第二步初始化发生在BeanPostProcessor.postProcessBeforeInitialization中。这个方法在任何实例的初始化之前进行。这个主要是对spring jdbc中的JdbcDaoSupport中的setDataSource进行注入(@datasource),注意,注入的不是统一的DataSource。而是具体的datasource(这也是本次设计的一个缺陷,这样,对于spring-jdbc的代码就无法实现动态切换数据源)。同时,也顺便检查下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;
}
统一数据源的初始化
下面开始进行具体的bean的实例话。首先看multiDataSource。主要是加载配置,初始化各个数据源。这个类在spring xml中配置,使用者一定会将其初始化。
public class MultiDataSources extends AbstractRoutingDataSource {
private void initializeDataSources() throws SQLException, IllegalAccessException {
dataSources = new HashMap<>();//内部自己维护的数据源和名称列表
//加载配置
Config dataSourceList = ConfigService.getConfig("XXX.DatasourceList");
this.resolveDataSourcesToSet(dataSourceList.getProperty("DataSources", null));
for (String dbtype : dataSourceNames) {
//转化为DRUID数据源,并保存在一个MAP中
DruidDataSource dataSource = this.dataSourceAssembler(dbtype);
dataSources.put(dbtype, dataSource);
targetDataSources.put(dbtype, dataSource);//将数据源交给AbstractRoutingDataSource来维护。这里似乎由改进重构空间?
}
}
private DruidDataSource dataSourceAssembler(String dbtype) throws IllegalAccessException, SQLException {
DruidDataSource dataSource = new DruidDataSource();
String namespace = config.getProperty(dbtype, null);
Config ds = ConfigService.getConfig(namespace);
//DataSourceConfigFilter负责处理用户名密码信息,这个类主要是从远程获取加密后的用户名密码然后本地解密。(当然,debug时候能看见。。)
//这个类不交给Spring容器,当然也不会被织入切面
dataSource.setFilters("com.gdl.dal.druid.DataSourceConfigFilter");
//初始化性能监控的Filter(StatFilter+GdlCatStatFilterAspect)
ArrayList<Filter> statFilters = new ArrayList<>();
statFilters.add(getDruidStatFilterProxy(ds));
dataSource.setProxyFilters(statFilters);
Properties connectProperties = new Properties();
connectProperties.setProperty("config.decrypt", "true");
connectProperties.setProperty("dbtype", dbtype);
connectProperties.setProperty("config.name", configPassword.getProperty(dbtype, null));
dataSource.setConnectProperties(connectProperties);
for(String propertyName : ds.getPropertyNames()) {
if("username".equals(propertyName) || "password".equals(propertyName) || "slowsqlmillis".equals(propertyName)) continue;
dataSourceAssemblerHelper(dataSource, propertyName, ds.getProperty(propertyName, null));
}
//监听配置更改事件
ds.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
updateDataSource(changeEvent);
}
});
return dataSource;
}
//更新设置
private void updateDataSource(ConfigChangeEvent changeEvent) {
DruidDataSource updateDS = (DruidDataSource)dataSources.get(namespaceDataSource.get(changeEvent.getNamespace()));
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
dataSourceAssemblerHelper(updateDS, change.getPropertyName(), change.getNewValue());
}
}
//根据设置注入属性
private void dataSourceAssemblerHelper(DruidDataSource dataSource, String propertyName, String propertyVal) {
try {
//处理slowsqlmillis属性reload逻辑。方法是把上次注册的filter取出来删掉,重新建立新设置的filter载入
if (propertyName.equalsIgnoreCase("slowsqlmillis")) {
try {
List<Filter> proxyFilters = dataSource.getProxyFilters();
List<Filter> newFilters = new ArrayList<>();
for (Filter f : proxyFilters) {
if (AopUtils.isAopProxy(f)) {
StatFilter statFilter = new StatFilter();
statFilter.setSlowSqlMillis(Long.valueOf(propertyVal));
AspectJProxyFactory factory = new AspectJProxyFactory(statFilter);
factory.addAspect(UmeCatStatFilterAspect.class);
newFilters.add((Filter)factory.getProxy());
} else {
newFilters.add(f);
}
}
dataSource.clearFilters();
dataSource.setProxyFilters(newFilters);
return;
} catch (Exception e) {
e.printStackTrace();
}
return;
}
if("username".equals(propertyName) || "password".equals(propertyName)) {
return;
}
Field field = dataSource.getClass().getSuperclass().getDeclaredField(propertyName);
field.setAccessible(true);
switch (TypeName.valueOf(field.getType().getSimpleName().toUpperCase())) {
case INT:
case INTEGER:
field.set(dataSource, Integer.valueOf(propertyVal));
break;
case LONG:
field.set(dataSource, Long.valueOf(propertyVal));
break;
case BOOLEAN:
field.set(dataSource, Boolean.valueOf(propertyVal));
break;
case STRING:
field.set(dataSource, propertyVal);
break;
}
} catch (NoSuchFieldException | IllegalAccessException e) {
}
}
}
使用拦截器具体化数据源(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 Object intercept(Invocation invocation) throws Throwable {
DataSource dataSource = Class.forName(className).getAnnotation(DataSource.class);
String dataSourceAnotationValue = dataSource.value();
MultiDataSourcesSwitcher.setDataSourceType(dataSourceAnotationValue);
return invocation.proceed();
}
拦截的主要目的是根据注解的值选定具体的数据源。数据源被保存在一个静态类ThreadLocal变量中。
public class MultiDataSourcesSwitcher {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType){
contextHolder.set(dataSourceType);
}
static String getDataSourceType(){
return contextHolder.get();
}
public static void clearDataSourceType(){
contextHolder.remove();
}
}
使用Druid的filter(主要是为了监控)
不熟悉Druid的同学请学习
https://blog.csdn.net/define_us/article/details/80625721
利用druid提供的StatFilter可以提供你对sql运行事件的一些监控。你可以继承该类。
然后在spring配置文件中作如下配置
<bean id="myStatFilterAspect" class="com.gdl.dal.druid.GdlStatFilterAspect" />
第一步,写一个切面,这个切面负责记录日志和监控。
@Aspect
@Component
public class GdlCatStatFilterAspect {
private final static String FILEHEADER = "/opt/applog/MskyLog/UmeDAL/SlowSql.";
private static Logger logger = LoggerFactory.getLogger(GdlCatStatFilterAspect.class);
private static String appid = RegexUtil.getAppid();
//拦截druid自带的Filter(这里其实就是StatFilter)
@Pointcut("execution(* com.alibaba.druid.filter.FilterEventAdapter.*_execute*(..))")
private void anyMethod() {
}
@Around("anyMethod()")
public Object process(ProceedingJoinPoint point) throws Throwable {
//日志和监控处理
}
完成这个切面似乎就大功告成了。但是我们还有另一个需求,就是比如说我们要动态改变StatFilter中慢sql的评价标准,这个参数要支持在线设置进去这个AOP的切面。方法很多,我们采取下面这种,就是在重新加载参数时,把上次的filter拿出来,删掉,建立新的filter,从新织入上面的切面。下面重温下我们刚刚介绍过的dataSourceAssemblerHelper
List<Filter> proxyFilters = dataSource.getProxyFilters();
List<Filter> newFilters = new ArrayList<>();
for (Filter f : proxyFilters) {
if (AopUtils.isAopProxy(f)) {
StatFilter statFilter = new StatFilter();
statFilter.setSlowSqlMillis(Long.valueOf(propertyVal));
AspectJProxyFactory factory = new AspectJProxyFactory(statFilter);
factory.addAspect(UmeCatStatFilterAspect.class);
newFilters.add((Filter)factory.getProxy());
} else {
newFilters.add(f);
}
}
dataSource.clearFilters();
dataSource.setProxyFilters(newFilters);
配置WEB监控
添加web-fragment.xml,以便可以搭车在web上显示
<servlet>
<servlet-name>DruidStatView </servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet </servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView </servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>
实际干活
是利用spring jdbc的AbstractRoutingDataSource支持多个数据源。在getconnection时,会根据上面的ThreadLocal选择具体的数据源。
public class MultiDataSources extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return MultiDataSourcesSwitcher.getDataSourceType();
}