从零开始构建数据访问层

目的

一般公司的数据访问层有多种方式,比如说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();
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值