spring之动态注册bean
前言一般而言,我们都知道,在启动一个spring/springboot项目时,随着ioc容器的加载,会向容器中注册许多我们在程序中已经定义好的bean,我们平时注册bean的方式如下:xml方式:<beans>//Bean定义的开始和结束<importresource=“resource1.xml” />//导入其他配置文件Bean的定义<importresource
前言
一般而言,我们都知道,在启动一个spring/springboot项目时,随着ioc容器的加载,会向容器中注册许多我们在程序中已经定义好的bean,我们平时注册bean的方式如下:
xml方式:
<beans>//Bean定义的开始和结束
<import resource=“resource1.xml” />//导入其他配置文件Bean的定义
<import resource=“resource2.xml” />
<bean id=“bean1” class=“***”></bean>
<bean name=“bean2” class=“***”></bean>
<alias alias=“bean3” name=“bean2” />//alias用于定义Bean的别名
</beans>
配置类方式:
@Bean(name = "storage")
public DataSourceProxy storageDataSourceProxy(@Qualifier("originStorage") DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
上述两种注册bean的方式,都属于静态的,也就是注册的目标bean都已经写死在我们的程序中,且项目一启动时,就会事先加载和创建这些bean,并注册到ioc容器中。
这也导致我们使用的这些bean,只能使用现有配置的属性和行为,不能做超出这之外的功能,这就出现了局限性。
当然,我们也可以将所有需要的属性配置好,在程序运行中,在读取这些配置动态的修改bean的属性,就可以使得这些bean完成其他操作了,无可厚非,这也是一种方法,但是还是有一定的局限性,假设这些配置有几万条呢。。。。
应用场景和解决问题
平常时,已有的静态bean,已经能满足我们工作的大量需求。但由于这些bean的属性和行为都固定了,在特殊需求下,有一定的局限性。
举个我遇到的工作场景,一个项目下需要连接许多库,这些库可以是mysql、oracle、sqlserver、Db2不同类型的库,每种类型下也有很多的库,用户可以根据自己的需求在页面选择目标库,实现切换,并可以对目标库的数据进行维护。
这里我们可以想到,平常时我们一个项目的数据源连接,直接配置在yml就行了。即便多个数据源,也是可以直接配置在yml就行了,但现在这个场景的库不仅一个,也不仅一个类型,有好几百个库,所有仅靠yml的配置,是无法满足这个需求了,这就可以考虑使用动态bean了。
这里,我们可以知道,动态bean主要解决的问题是:在程序执行过程中,动态的配置bean信息且动态的向容器中注册bean,解决静态注册bean的局限性。(只能意会,难以言传)
了解注册Bean的基本原理
我们先不考虑上述的场景,我们先学习如何注册bean。
我们先来参考一位博主的文章,原文链接:https://blog.csdn.net/liuyueyi25/article/details/83244255
下面的内容,我是直接搬过来了,请往下看。
1. 注册bean的核心实现类
我们的实现方式和上面也没什么区别,依然是借助BeanDefinition来创建Bean定义并注册到BeanFactory中,具体实现的核心代码如下:
public class ManualRegistBeanUtil {
/**
* 主动向Spring容器中注册bean
*
* @param applicationContext Spring容器
* @param name BeanName
* @param clazz 注册的bean的类性
* @param args 构造方法的必要参数,顺序和类型要求和clazz中定义的一致
* @param <T>
* @return 返回注册到容器中的bean对象
*/
public static <T> T registerBean(ConfigurableApplicationContext applicationContext, String name, Class<T> clazz,
Object... args) {
if(applicationContext.containsBean(name)) {
Object bean = applicationContext.getBean(name);
if (bean.getClass().isAssignableFrom(clazz)) {
return (T) bean;
} else {
throw new RuntimeException("BeanName 重复 " + name);
}
}
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
for (Object arg : args) {
beanDefinitionBuilder.addConstructorArgValue(arg);
}
BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
BeanDefinitionRegistry beanFactory = (BeanDefinitionRegistry) applicationContext.getBeanFactory();
beanFactory.registerBeanDefinition(name, beanDefinition);
return applicationContext.getBean(name, clazz);
}
}
上面唯一的方法中,接收四个参数,源码中也有说明,稍微需要注意下的是Spring容器中不允许出现同名的Bean.
(我们看了这位博主写的这个方法,BeanDefinitionBuilder 的方式实现注册,而该方法也没有对目标bean做任何操作,直接注册了,这里与静态注册bean的原理相当)
2. bean的定义
a. 不依赖其他Bean
即不依赖其他的Bean, 单纯的供其他地方使用,这种情况下,主要需要测试的就是别人可以通过什么方式来使用它
@Slf4j
public class ManualBean {
private int id;
public ManualBean() {
Random random = new Random();
id = random.nextInt(100);
}
public String print(String msg) {
return "[ManualBean] print : " + msg + " id: " + id;
}
}
b. 依赖其他Bean
和前面一个不同,这个Bean内部需要注入其他的Bean,因此我们主动注册Bean时,能否将依赖的Bean也注入进去呢?
定义一个测试Bean
@Slf4j
public class ManualDIBean {
private int id;
@Autowired
private OriginBean originBean;
private String name;
public ManualDIBean(String name) {
Random random = new Random();
this.id = random.nextInt(100);
this.name = name;
}
public String print(String msg) {
String o = originBean.print(" call by ManualDIBean! ");
return "[ManualDIBean] print: " + msg + " id: " + id + " name: " + name + " originBean print:" + o;
}
}
(显然,是可以的;因为在容器加载时,originBean就已经被注册到容器了,因此这里自然能注入)
c. 普通Bean依赖手动注册的Bean
这个其实就是使用case了,手动注册的Bean也是被人使用的,那可以怎么使用呢?传统的Autowired可否?
@Slf4j
@Component
public class AnoOriginBean {
// 希望可以注入 主动注册的Bean
@Autowired
private ManualBean manualBean;
public AnoOriginBean() {
System.out.println("AnoOriginBean init: " + System.currentTimeMillis());
}
public String print() {
return "[AnoOriginBean] print!!! manualBean == null ? " + (manualBean == null);
}
}
Bean注册实现
前面定义了两个需要手动注册的bean,所以就需要选择一个合适的地方来处理主动注册的逻辑,我们把这段逻辑放在AutoConfig中,用于测试演示
@Configuration
public class BeanRegisterAutoConf {
public BeanRegisterAutoConf(ApplicationContext applicationContext) {
System.out.println("BeanRegisterAutoConf init: " + System.currentTimeMillis());
registerManualBean((ConfigurableApplicationContext) applicationContext);
}
/**
* 手动注册自定义地bean
* @param applicationContext
*/
private void registerManualBean(ConfigurableApplicationContext applicationContext) {
// 主动注册一个没什么依赖的Bean
ManualBean manualBean = ManualRegistBeanUtil.registerBean(applicationContext, "manualBean", ManualBean.class);
manualBean.print("test print manualBean");
// manualDIBean 内部,依赖由Spring容器创建的OriginBean
ManualDIBean manualDIBean = ManualRegistBeanUtil.registerBean(applicationContext, "manualDIBean",
ManualDIBean.class, "依赖OriginBean的自定义Bean");
manualDIBean.print("test print manualDIBean");
}
}
(这里使用了一个配置类,调用我们之前定义的注册bean的方法,将我们定义的bean注册到容器里了,因此自然可以使用@Autowired)
小结
参考这篇博文,我们已经大致了解了bean的注册原理,但要解决我们上面的场景,仍旧是不行的,因此我们需要使用动态注册bean,动态注册bean也就是在上述的基础上稍稍加一点逻辑而已。
动态注册bean
场景
回忆一下场景:
一个项目下需要连接许多库,这些库可以是mysql、oracle、sqlserver、Db2不同类型的库,每种类型下也有很多的库,用户可以根据自己的需求在页面选择目标库,实现切换,并可以对目标库的数据进行维护。
这里我们可以想到,平常时我们一个项目的数据源连接,直接配置在yml就行了。即便多个数据源,也是可以直接配置在yml就行了,但现在这个场景的库不仅一个,也不仅一个类型,有好几百个库,所有仅靠yml的配置,是无法满足这个需求了,这就可以考虑使用动态bean了。
分析
已知项目使用的数据源是DruidDataSource,若平常时一般的数据源,我们直接在yml里配置即可,项目启动后,springboot就会给我们创建一个数据源对象于容器中供我们使用。但现今,我们不采用这种方式,因此就需要我们通过动态注册bean的方式,实现动态注册数据源。
(目标清楚了:就是实现动态注册DruidDataSource数据源)
我们项目需要连接的库有很多,那么这些库的连接信息,应该存在哪?在哪取?怎么用?
自然的存在数据库,从库里取,所有需要有一个表存储这些信息,对应也需要一个实体类。
(本项目自然会有一个独立的数据库,负责存储我们项目运行需要的信息,那些数据库连接配置信息,自然也是存在这个库里)
由上述两个分析,我们已经能拿到一个动态的数据源了,但问题来了,拿到数据源后怎么用呢?
也许大家平时都用ssm/ssh等框架,使用起来也很简单,操作数据库也就是定义sql写方法即可。但是大家也许不知道,操作数据库这一过程,并非仅有表面调用方法这么简单,我们先看一下原始spring+mybatis配置数据源的xml:
<!--读入配置文件 -->
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath*:jdbc.properties</value>
</property>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!-- 文件映射器,指定类文件 -->
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
<!-- 自动扫描mapping.xml文件 -->
<property name="mapperLocations" value="classpath:com/chinasoft/ssm/mapper/*.xml"></property>
</bean> <!--下面两种方法,一种是自动是扫描对应的service类,一种是通过注入sqlsessionFactory的方式来获取一个SqlsessionTemplate-->
<!-- DAO接口所在包名,Spring会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--set注入-->
<property name="basePackage" value="com.chinasoft.ssm.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
<!-- 配置SqlSessionTemplate -->
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<!-- 通过构造函数注入 -->
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
从这个xml,我们就可以看到,这个xml文件配置了一个sqlSessionFactory的东西,而我们的调用的所有mapper方法去操作数据库,其实都跳不过它,都需要它的支持,它负责了我们程序与数据库的交互。
(我们动态的注册了数据源bean,自然还要动态的注册sqlSessionFactoryBean,不然怎么使用)
实现
下面只展示的几个关键步骤。
动态注册数据源DruidDataSource和session工厂sqlSessionFactoryBean
private synchronized void initTargetPool(Database database) {
String wDataSourceId = database.getName() + database.getSeq() + database.getMode() + DATASOURCE_NAME;
String wSqlSessionFactoryId = database.getName() + database.getSeq() + database.getMode();
BeanDefinitionBuilder wBeanDefBuilder = null;
wBeanDefBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyDataSource.class);
wBeanDefBuilder.getBeanDefinition().setAttribute(ID, wDataSourceId);
wBeanDefBuilder.setScope(SINGLETON);
wBeanDefBuilder.addPropertyValue(URL, database.getUrl());
wBeanDefBuilder.addPropertyValue(USERNAME, database.getUser());
wBeanDefBuilder.addPropertyValue(DB_PW, database.getPassword());
wBeanDefBuilder.addPropertyValue(DRIVERCLASSNAME, EnumType.DriverClass.getValue(database.getType()));
wBeanDefBuilder.addPropertyValue(DEFAULTAUTOCOMMIT, false);
wBeanDefBuilder.addPropertyValue(INITIALSIZE, 1);
wBeanDefBuilder.addPropertyValue(MINIDLE, 1);
wBeanDefBuilder.addPropertyValue(MAXACTIVE, 10);
wBeanDefBuilder.addPropertyValue("testWhileIdle", false);
wBeanDefBuilder.addPropertyValue("testOnBorrow", true);
wBeanDefBuilder.addPropertyValue("maxWait", 30000);
// wBeanDefBuilder.addPropertyValue("validationQuery", getCheckSql(database.getType()));
Properties properties = new Properties();
switch (database.getType()) {
case ORACLE:
properties.setProperty("v$session.program", "dbmu");
break;
case DB2:
properties.setProperty("clientProgramName", "dbmu");
break;
case MYSQL:
properties.setProperty("connectionAttributes", "program_name:dbmu");
break;
case SQLSERVER:
properties.setProperty("applicationName", "dbmu");
break;
default:
throw new AppException("DBMU000", "不支持的数据库类型");
}
wBeanDefBuilder.addPropertyValue("connectProperties", properties);
//注册数据源
SpringUtil.getApplicationContext().registerBeanDefinition(wDataSourceId,wBeanDefBuilder.getRawBeanDefinition());
Object wDataSourceBean = SpringUtil.getApplicationContext().getBean(wDataSourceId);
wBeanDefBuilder = BeanDefinitionBuilder.genericBeanDefinition(SqlSessionFactoryBean.class);
wBeanDefBuilder.getBeanDefinition().setAttribute(ID, wSqlSessionFactoryId);
wBeanDefBuilder.addPropertyValue(DATASOURCE, wDataSourceBean);
try {
wBeanDefBuilder.addPropertyValue("mapperLocations",new PathMatchingResourcePatternResolver().getResources(String.format(DAO_PATH, database.getType().toLowerCase())));
} catch (Exception e) {
throw new AppException("DBMU000", "添加数据库配置文件失败:" + e.getMessage());
}
wBeanDefBuilder.addPropertyValue("plugins", interceptorsProvider.getIfAvailable());
// 注册SqlSessionFactoryId
SpringUtil.getApplicationContext().registerBeanDefinition(wSqlSessionFactoryId,wBeanDefBuilder.getRawBeanDefinition());
}
创建模型
public class Database {
private String name = "";
private Integer seq = 0;
private String type = "";
private String description = "";
private String timezone = "";
private String team = "";
private String archCode = "";
private String connectType = "";
private String instanceName = "";
private String databaseManager = "";
private String appDeveloper = "";
private String serverHost = "";
private String serverIp = "";
private String serverPort = "";
private String url = "";
private String user = "";
private String password = "";
private String synchronizeTime = "";
}
自然是贴表走,表的sql就不说了。
使用上述注册的bean操作数据库
public void singleExecute(Database database, String operateType, String executeSql) {
DefaultSqlSessionFactory bean = (DefaultSqlSessionFactory) SpringUtil.getApplicationContext().getBean(database.getName() + database.getSeq() + database.getMode());
SqlSession sqlSession = bean.openSession();
String sql = "";
sqlSession.getMapper(ExecuteMapper.class).executeInsert(sql);
}
mapper方法及xml
@Mapperpublic interface ExecuteMapper {
int executeUpdate(@Param("executeSql") String executeSql);
int executeDelete(@Param("executeSql") String executeSql);
int executeInsert(@Param("executeSql") String executeSql);
List<List<Field>> executeQuery(@Param("querySql") String querySql);
}
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmb.dbmu.dao.database.ExecuteMapper">
<update id="executeUpdate" parameterType="java.util.Map">
${executeSql}
</update>
<delete id="executeDelete" parameterType="java.util.Map" >
${executeSql}
</delete>
<insert id="executeInsert" parameterType="java.util.Map" >
${executeSql}
</insert>
<select id="executeQuery" parameterType="java.util.Map" resultType="object">
${querySql}
</select>
</mapper>
小结
上述的内容就是动态注册bean和使用了。
更多推荐
所有评论(0)