spring注解 @PropertySource配置数据源全流程


Posted in Java/Android onMarch 25, 2022

@PropertySource数据源配置

一般在配置数据源是都会使用xml的方式注入,key-value在properties中管理;spring4.X已有着比较完善的注解来替换xml的配置方式。

使用xml配置数据源

通常我们使用xml配置数据源,使用SpEL获取properties中的配置。

applicationContext.xml 中配置 dataSource 及 PreferencesPlaceholderConfigurer,使用 PropertyPlaceholderConfigurer进行Bean属性替换

<bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <list>
                <value>classpath:/jdbc.properties</value>
            </list>
        </property>
        <property name="fileEncoding" value="utf-8"/>
    </bean>
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
    <property name="properties" ref="configProperties" />
</bean>
<!-- 使用proxool连接池的数据源, -->
<bean id="dataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource">
    <!-- 数据源的别名 -->
    <property name="alias" value="${proxool.alias}" /> 
    <!-- 驱动 -->
    <property name="driver" value="${proxool.driver}" /> 
    <!-- 链接URL  -->
    <property name="driverUrl" value="${proxool.driverUrl}" /> 
    <!-- 用户名-->
    <property name="user" value="${proxool.user}" />
    <!-- 密码 -->
    <property name="password" value="${proxool.password}" /> 
    <!-- 最大链接数-->
    <property name="maximumConnectionCount" value="${proxool.maximumConnectionCount}" /> 
    <!-- 最小链接数 -->
    <property name="minimumConnectionCount" value="${proxool.minimumConnectionCount}" /> 
    <!-- ...(略) -->
</bean> 

jdbc.properties

proxool.alias=mySql
proxool.driver=com.mysql.jdbc.Driver
proxool.driverUrl=jdbc:mysql://localhost:3306/test?characterEncoding=utf8
proxool.user=root
proxool.password=root
proxool.maximumActiveTime=1200
proxool.maximumConnectionCount=50
#...

使用javaBean配置数据源

DataSourceConfiguration类是数据源的javaBean配置方式,@Configuratio注解当前类,

spring启动时会扫描被@Configuratio注解的类,注入当前类中配置的方法bean;

当然别忘了启用注解扫描:

<context:annotation-config/>  
<context:component-scan base-package="com.XXX.test.dateSource"></context:component-scan>

@value注解读取配置

@value中可以直接使用SpEL,获取properties配置,成员变量也不需要getter、setter,不过还是有一个前提,需要配置xml:

<bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <list>
                <value>classpath:/jdbc.properties</value>
            </list>
        </property>
        <property name="fileEncoding" value="utf-8"/>
    </bean>
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
    <property name="properties" ref="configProperties" />
</bean>

@Bean注解:spring扫面当前类时,注入每个有@Bean注解的方法的返回值Bean, name属性默认为返回值类类名首字母小写,这里自己设置name。

package com.XXX.test.dateSource;
import org.logicalcobwebs.proxool.ProxoolDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuratio
public class DataSourceConfiguration{
    @Value("${proxool.alias}")
    private String alias;
    @Value("${proxool.driver}")
    private String driver;
    @Value("${proxool.driverUrl}")
    private String driverUrl;
    @Value("${proxool.user}")
    private String user;
    @Value("${proxool.password}")
    private String password;
    //...
    @Bean(name="dataSource")
    public ProxoolDataSource dataSource(){
         ProxoolDataSource proxoolDataSource = new ProxoolDataSource();
         proxoolDataSource.setDriver(driver);
         proxoolDataSource.setDriverUrl(driverUrl);
         proxoolDataSource.setUser(user);
         proxoolDataSource.setPassword(password);
         //...
         return proxoolDataSource;
     }
 }

这时dataSource已被注入,使用时可注解注入,如下:

    @Autowired
    private ProxoolDataSource dataSource;

@PropertySource注解读取配置

@PropertySource注解当前类,参数为对应的配置文件路径,这种方式加载配置文件,可不用在xml中配置PropertiesFactoryBean引入jdbc.properties,使用时方便得多,DataSourceConfiguration不再需要成员变量,取而代之的是需要注入一个Environment环境配置,使用env.getProperty(key)获取数据:

package com.XXX.test.dateSource;
import org.logicalcobwebs.proxool.ProxoolDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
@Configuratio
@PropertySource("classpath:/jdbc.properties")
public class DataSourceConfiguration{
    @Autowired
    private Environment env;
    @Bean(name="dataSource")
    public ProxoolDataSource dataSource(){
         ProxoolDataSource proxoolDataSource = new ProxoolDataSource();
         proxoolDataSource.setDriver(env.getProperty("proxool.alias"));
         proxoolDataSource.setDriverUrl(env.getProperty("proxool.driver"));
         proxoolDataSource.setUser(env.getProperty("proxool.user"));
         proxoolDataSource.setPassword(env.getProperty("proxool.password"));
         //...
         return proxoolDataSource;
     }
 }

这里主要是说明注解的用法,所以没有具体体现数据源全部参数的配置。对于有强迫症的来说若项目中所有bean都使用注解,几乎不太希望仅dataSource用xml类配置,换成类的方式类配置强迫感就消失了! 

注解的spring多数据源配置及使用

前一段时间研究了一下spring多数据源的配置和使用,为了后期从多个数据源拉取数据定时进行数据分析和报表统计做准备。由于之前做过的项目都是单数据源的,没有遇到这种场景,所以也一直没有去了解过如何配置多数据源。

后来发现其实基于spring来配置和使用多数据源还是比较简单的,因为spring框架已经预留了这样的接口可以方便数据源的切换。

先看一下spring获取数据源的源码

spring注解 @PropertySource配置数据源全流程

可以看到AbstractRoutingDataSource获取数据源之前会先调用determineCurrentLookupKey方法查找当前的lookupKey,这个lookupKey就是数据源标识。

因此通过重写这个查找数据源标识的方法就可以让spring切换到指定的数据源了。

第一步:创建一个DynamicDataSource的类

继承AbstractRoutingDataSource并重写determineCurrentLookupKey方法,代码如下:

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 从自定义的位置获取数据源标识
        return DynamicDataSourceHolder.getDataSource();
    }
}

第二步:创建DynamicDataSourceHolder

用于持有当前线程中使用的数据源标识,代码如下:

public class DynamicDataSourceHolder {
    /**
     * 注意:数据源标识保存在线程变量中,避免多线程操作数据源时互相干扰
     */
    private static final ThreadLocal<String> THREAD_DATA_SOURCE = new ThreadLocal<String>();
    public static String getDataSource() {
        return THREAD_DATA_SOURCE.get();
    }
    public static void setDataSource(String dataSource) {
        THREAD_DATA_SOURCE.set(dataSource);
    }
    public static void clearDataSource() {
        THREAD_DATA_SOURCE.remove();
    }
}

第三步:配置多个数据源

和第一步里创建的DynamicDataSource的bean,简化的配置如下:

<!--创建数据源1,连接数据库db1 -->
<bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${db1.driver}" />
    <property name="url" value="${db1.url}" />
    <property name="username" value="${db1.username}" />
    <property name="password" value="${db1.password}" />
</bean>
<!--创建数据源2,连接数据库db2 -->
<bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${db2.driver}" />
    <property name="url" value="${db2.url}" />
    <property name="username" value="${db2.username}" />
    <property name="password" value="${db2.password}" />
</bean>
<!--创建数据源3,连接数据库db3 -->
<bean id="dataSource3" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${db3.driver}" />
    <property name="url" value="${db3.url}" />
    <property name="username" value="${db3.username}" />
    <property name="password" value="${db3.password}" />
</bean>
<bean id="dynamicDataSource" class="com.test.context.datasource.DynamicDataSource">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <!-- 指定lookupKey和与之对应的数据源 -->
            <entry key="dataSource1" value-ref="dataSource1"></entry>
            <entry key="dataSource2" value-ref="dataSource2"></entry>
            <entry key="dataSource3 " value-ref="dataSource3"></entry>
        </map>
    </property>
    <!-- 这里可以指定默认的数据源 -->
    <property name="defaultTargetDataSource" ref="dataSource1" />
</bean>

到这里已经可以使用多数据源了,在操作数据库之前只要DynamicDataSourceHolder.setDataSource("dataSource2")即可切换到数据源2并对数据库db2进行操作了。

示例代码如下:

@Service
public class DataServiceImpl implements DataService {
    @Autowired
    private DataMapper dataMapper;
    @Override
    public List<Map<String, Object>> getList1() {
        // 没有指定,则默认使用数据源1
        return dataMapper.getList1();
    }
    @Override
    public List<Map<String, Object>> getList2() {
        // 指定切换到数据源2
        DynamicDataSourceHolder.setDataSource("dataSource2");
        return dataMapper.getList2();
    }
    @Override
    public List<Map<String, Object>> getList3() {
        // 指定切换到数据源3
        DynamicDataSourceHolder.setDataSource("dataSource3");
        return dataMapper.getList3();
    }
}

----------------------------华丽的分割线----------------------------

但是问题来了,如果每次切换数据源时都调用DynamicDataSourceHolder.setDataSource("xxx")就显得十分繁琐了,而且代码量大了很容易会遗漏,后期维护起来也比较麻烦。能不能直接通过注解的方式指定需要访问的数据源呢,比如在dao层使用@DataSource("xxx")就指定访问数据源xxx?当然可以!前提是,再加一点额外的配置^_^。

首先,我们得定义一个名为DataSource的注解,代码如下:

@Target({ TYPE, METHOD })
@Retention(RUNTIME)
public @interface DataSource {
    String value();
}

然后,定义AOP切面以便拦截所有带有注解@DataSource的方法,取出注解的值作为数据源标识放到DynamicDataSourceHolder的线程变量中:

public class DataSourceAspect {
    /**
     * 拦截目标方法,获取由@DataSource指定的数据源标识,设置到线程存储中以便切换数据源
     *
     * @param point
     * @throws Exception
     */
    public void intercept(JoinPoint point) throws Exception {
        Class<?> target = point.getTarget().getClass();
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 默认使用目标类型的注解,如果没有则使用其实现接口的注解
        for (Class<?> clazz : target.getInterfaces()) {
            resolveDataSource(clazz, signature.getMethod());
        }
        resolveDataSource(target, signature.getMethod());
    }
    /**
     * 提取目标对象方法注解和类型注解中的数据源标识
     *
     * @param clazz
     * @param method
     */
    private void resolveDataSource(Class<?> clazz, Method method) {
        try {
            Class<?>[] types = method.getParameterTypes();
            // 默认使用类型注解
            if (clazz.isAnnotationPresent(DataSource.class)) {
                DataSource source = clazz.getAnnotation(DataSource.class);
                DynamicDataSourceHolder.setDataSource(source.value());
            }
            // 方法注解可以覆盖类型注解
            Method m = clazz.getMethod(method.getName(), types);
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                DataSource source = m.getAnnotation(DataSource.class);
                DynamicDataSourceHolder.setDataSource(source.value());
            }
        } catch (Exception e) {
            System.out.println(clazz + ":" + e.getMessage());
        }
    }
}

最后在spring配置文件中配置拦截规则就可以了,比如拦截service层或者dao层的所有方法:

<bean id="dataSourceAspect" class="com.test.context.datasource.DataSourceAspect" />
    <aop:config>
        <aop:aspect ref="dataSourceAspect">
            <!-- 拦截所有service方法 -->
            <aop:pointcut id="dataSourcePointcut" expression="execution(* com.test.*.dao.*.*(..))"/>
            <aop:before pointcut-ref="dataSourcePointcut" method="intercept" />
        </aop:aspect>
    </aop:config>
</bean>

OK,这样就可以直接在类或者方法上使用注解@DataSource来指定数据源,不需要每次都手动设置了。

示例代码如下:

@Service
// 默认DataServiceImpl下的所有方法均访问数据源1
@DataSource("dataSource1")
public class DataServiceImpl implements DataService {
    @Autowired
    private DataMapper dataMapper;
    @Override
    public List<Map<String, Object>> getList1() {
        // 不指定,则默认使用数据源1
        return dataMapper.getList1();
    }
    @Override
    // 覆盖类上指定的,使用数据源2
    @DataSource("dataSource2")
    public List<Map<String, Object>> getList2() {
        return dataMapper.getList2();
    }
    @Override
    // 覆盖类上指定的,使用数据源3
    @DataSource("dataSource3")
    public List<Map<String, Object>> getList3() {
        return dataMapper.getList3();
    }
}

提示:注解@DataSource既可以加在方法上,也可以加在接口或者接口的实现类上,优先级别:方法>实现类>接口。也就是说如果接口、接口实现类以及方法上分别加了@DataSource注解来指定数据源,则优先以方法上指定的为准。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Java/Android 相关文章推荐
Java输出Hello World完美过程解析
Jun 13 Java/Android
Spring boot应用启动后首次访问很慢的解决方案
Jun 23 Java/Android
简单总结SpringMVC拦截器的使用方法
Jun 28 Java/Android
java基础——多线程
Jul 03 Java/Android
Java使用jmeter进行压力测试
Jul 09 Java/Android
小程序与后端Java接口交互实现HelloWorld入门
Jul 09 Java/Android
Java移除无效括号的方法实现
Aug 07 Java/Android
使用Java去实现超市会员管理系统
Mar 18 Java/Android
Android Flutter实现图片滑动切换效果
Apr 07 Java/Android
Java设计模式之代理模式
Apr 22 Java/Android
详解Spring Security如何在权限中使用通配符
Jun 28 Java/Android
MyBatis XPathParser解析器使用范例详解
Jul 15 Java/Android
Netty客户端接入流程NioSocketChannel创建解析
Mar 25 #Java/Android
Java 超详细讲解设计模式之中的抽象工厂模式
Netty分布式客户端处理接入事件handle源码解析
Java 超详细讲解IO操作字节流与字符流
Netty分布式客户端接入流程初始化源码分析
Mar 25 #Java/Android
java后台调用接口及处理跨域问题的解决
Mar 24 #Java/Android
SpringBoot中使用Redis作为全局锁示例过程
Mar 24 #Java/Android
You might like
Protoss建筑一览
2020/03/14 星际争霸
基于mysql的论坛(1)
2006/10/09 PHP
使用php 获取时间今天明天昨天时间戳的详解
2013/06/20 PHP
jQuery+PHP发布的内容进行无刷新分页(Fckeditor)
2015/10/22 PHP
laravel 数据验证规则详解
2019/10/23 PHP
在Laravel中使用MongoDB的方法示例
2019/11/11 PHP
JsEasy简介 JsEasy是什么?与下载
2007/03/07 Javascript
jQuery使用手册之 事件处理
2007/03/24 Javascript
JavaScript 基于原型的对象(创建、调用)
2009/10/16 Javascript
修复ie8&amp;chrome下window的resize事件多次执行
2011/10/20 Javascript
js中Math之random,round,ceil,floor的用法总结
2013/12/26 Javascript
JS实现可缩放、拖动、关闭和最小化的浮动窗口完整实例
2015/03/04 Javascript
DOM操作一些常用的属性汇总
2015/03/13 Javascript
JQuery select(下拉框)操作方法汇总
2015/04/15 Javascript
JS实现模拟百度搜索“2012世界末日”网页地震撕裂效果代码
2015/10/31 Javascript
create-react-app修改为多页面支持的方法
2018/05/17 Javascript
a标签调用js的方法总结
2019/09/05 Javascript
arcgis.js控制地图地体的显示范围超出区域自动弹回(实现思路)
2021/01/28 Javascript
Python 查看list中是否含有某元素的方法
2018/06/27 Python
python/sympy求解矩阵方程的方法
2018/11/08 Python
pytorch 在网络中添加可训练参数,修改预训练权重文件的方法
2019/08/17 Python
VSCode基础使用与VSCode调试python程序入门的图文教程
2020/03/30 Python
Python多分支if语句的使用
2020/09/03 Python
Python中全局变量和局部变量的理解与区别
2021/02/07 Python
销售辞职报告范文
2014/01/12 职场文书
坚定理想信念心得体会
2014/03/11 职场文书
关心下一代工作先进事迹
2014/08/15 职场文书
求职自荐信范文(优秀篇)
2015/03/27 职场文书
药品开票员岗位职责
2015/04/15 职场文书
2015年度村委会工作总结
2015/04/29 职场文书
学困生帮扶工作总结
2015/08/13 职场文书
SQLServer2008提示评估期已过解决方案
2021/04/12 SQL Server
浅谈移动端中的视口(viewport)的具体使用
2021/04/13 HTML / CSS
vue使用v-model进行跨组件绑定的基本实现方法
2021/04/28 Vue.js
关于@OnetoMany关系映射的排序问题,使用注解@OrderBy
2021/12/06 Java/Android
python manim实现排序算法动画示例
2022/08/14 Python