抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

SpringBoot整合其他组件

整合junit

POM文件添加

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

新建测试类

1
2
3
4
5
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes={app.class})
public class DemoTest {

}

整合Servlet、Filter、Listener

SpringBoot中有两种方式可以添加 Servlet、Filter、Listener。

代码注册

通过ServletRegistrationBean、 FilterRegistrationBean 和 ServletListenerRegistrationBean 获得控制。

WebConfig

新建WebConfig 类,用于bean的注入,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

@Configuration
public class WebConfig {

/**
* servletRegistrationBean:(使用代码注册Servlet(不需要@ServletComponentScan注解)).
*/
@Bean
public ServletRegistrationBean servletRegistrationBean() {
ServletRegistrationBean registrationBean = new ServletRegistrationBean();
registrationBean.setServlet(new XbqServlet());
List<String> urlMappings = new ArrayList<String>();
// 访问,可以添加多个
urlMappings.add("/xbq/servlet");
registrationBean.setUrlMappings(urlMappings);
registrationBean.setLoadOnStartup(1);
return registrationBean;
}

/**
* getDemoFilter:(使用代码注册拦截器).
*/
@Bean
public FilterRegistrationBean getDemoFilter() {
XbqFilter demoFilter = new XbqFilter();
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(demoFilter);
List<String> urlPatterns = new ArrayList<String>();
urlPatterns.add("/*"); //拦截路径,可以添加多个
registrationBean.setUrlPatterns(urlPatterns);
registrationBean.setOrder(1);
return registrationBean;
}

/**
* getDemoListener:(使用代码 引用 监听器).
*/
@Bean
public ServletListenerRegistrationBean<EventListener> getDemoListener() {
ServletListenerRegistrationBean<EventListener> registrationBean = new ServletListenerRegistrationBean<>();
registrationBean.setListener(new XbqListener());
registrationBean.setOrder(1);
return registrationBean;
}
}
XbqServlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class XbqServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body><center>");
out.println("<h3>我是通过代码注册Servlet</h3>");
out.println("</center></body>");
out.println("</html>");
}
}
XbqFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class XbqFilter implements Filter {

private static Logger logger = LoggerFactory.getLogger(XbqFilter.class);

@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("--xbq--初始化JoeFilter!");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)request;
logger.info(req.getRequestURL() + "---xbq---> doFilter ");
chain.doFilter(request, response);
}

@Override
public void destroy() {
logger.info("--xbq--销毁JoeFilter!");
}

}
XbqListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class XbqListener implements ServletContextListener {

private static Logger logger = LoggerFactory.getLogger(XbqListener.class);

@Override
public void contextInitialized(ServletContextEvent sce) {
logger.info("--xbq-监听器-ServletContext 初始化");
logger.info(sce.getServletContext().getServerInfo());
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
logger.info("--xbq-监听器-ServletContext 销毁");
}
}

通过注解自动注册

在 SpringBootApplication 上使用@ServletComponentScan 注解后,Servlet、Filter、Listener 可以直接通过 @WebServlet(urlPatterns = “/test/*”)、@WebFilter、@WebListener 注解自动注册,这些注解都是JDK的,无需其他代码。

配置启动类

在启动类添加@ServletComponentScan

1
2
3
@SpringBootApplication
@ServletComponentScan
public class DemoApplication
JoeServlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@WebServlet(urlPatterns = "/joe/*")
public class JoeServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body><center>");
out.println("<h3>我是通过 @WebServlet 注解注册Servlet的</h3>");
out.println("</center></body>");
out.println("</html>");
}
}
JoeFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@WebFilter(filterName = "joeFilter", urlPatterns = "/*")
public class JoeFilter implements Filter {

private static Logger logger = LoggerFactory.getLogger(JoeFilter.class);

@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("--joe--初始化JoeFilter!");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
logger.info(req.getRequestURL() + "---joe---> doFilter");
chain.doFilter(request, response);
}

@Override
public void destroy() {
logger.info("--joe--销毁JoeFilter!");
}
}
JoeListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebListener
public class JoeListener implements ServletContextListener {

private static Logger logger = LoggerFactory.getLogger(JoeListener.class);

@Override
public void contextInitialized(ServletContextEvent sce) {
logger.info("--Joe-监听器-ServletContext 初始化");
logger.info(sce.getServletContext().getServerInfo());
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
logger.info("--Joe-监听器-ServletContext 销毁");
}
}

springboot 整合 druid

Druid是一个非常优秀的连接池,非常好的管理了数据库连接,可以实时监控 数据库连接对象和应用程序的数据库操作记录

单数据源配置

导入POM
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
数据源相关配置
数据源属性配置

可以直接使用上面在配置文件中的配置,然后借助springboot自动装配机制完成配置加载。但是直接默认使用springboot自动加载的话,有时候容易出现bug,最好自己封装下,还可以灵活配置调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 数据库访问配置
# 主数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
######################### Druid连接池的配置信息 #################
#初始化连接大小
spring.druid.initialSize=5
#最小连接池数量
spring.druid.minIdle=5
#最大连接池数量
spring.druid.maxActive=20
#获取连接时最大等待时间,单位毫秒
spring.druid.maxWait=60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.druid.timeBetweenEvictionRunsMillis=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
spring.druid.minEvictableIdleTimeMillis=300000
#测试连接
spring.druid.validationQuery=SELECT 1 FROM DUAL
#申请连接的时候检测,建议配置为true,不影响性能,并且保证安全性
spring.druid.testWhileIdle=true
#获取连接时执行检测,建议关闭,影响性能
spring.druid.testOnBorrow=false
#归还连接时执行检测,建议关闭,影响性能
spring.druid.testOnReturn=false
#是否开启PSCache,PSCache对支持游标的数据库性能提升巨大,oracle建议开启,mysql下建议关闭
spring.druid.poolPreparedStatements=false
#开启poolPreparedStatements后生效
spring.druid.maxPoolPreparedStatementPerConnectionSize=20
#配置扩展插件,常用的插件有=>stat:监控统计 log4j:日志 wall:防御sql注入
spring.druid.filters=stat,wall,log4j
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.druid.connectionProperties='druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000'

数据源配置类

可以直接使用上面在配置文件中的配置,然后借助springboot自动装配机制完成配置加载。但是直接默认使用springboot自动加载的话,有时候容易出现bug,最好自己封装下,还可以灵活配置调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@Configuration
public class DruidConfiguration {
@Value("${spring.datasource.url}")
private String url;

@Value("${spring.datasource.username}")
private String username;

@Value("${spring.datasource.password}")
private String password;

@Value("${spring.datasource.driverClassName}")
private String driverClassName;

@Value("${spring.druid.initialSize}")
private int initialSize;

@Value("${spring.druid.minIdle}")
private int minIdle;

@Value("${spring.druid.maxActive}")
private int maxActive;

@Value("${spring.druid.maxWait}")
private int maxWait;

@Value("${spring.druid.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;

@Value("${spring.druid.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;

@Value("${spring.druid.validationQuery}")
private String validationQuery;

@Value("${spring.druid.testWhileIdle}")
private boolean testWhileIdle;

@Value("${spring.druid.testOnBorrow}")
private boolean testOnBorrow;

@Value("${spring.druid.testOnReturn}")
private boolean testOnReturn;

@Value("${spring.druid.poolPreparedStatements}")
private boolean poolPreparedStatements;

@Value("${spring.druid.maxPoolPreparedStatementPerConnectionSize}")
private int maxPoolPreparedStatementPerConnectionSize;

@Value("${spring.druid.filters}")
private String filters;

@Value("{spring.druid.connectionProperties}")
private String connectionProperties;

@Bean
@Primary
public DataSource dataSource() {
DruidDataSource datasource = new DruidDataSource();

datasource.setUrl(url);
datasource.setUsername(username);
datasource.setPassword(password); //这里可以做加密处理
datasource.setDriverClassName(driverClassName);

//configuration
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setValidationQuery(validationQuery);
datasource.setTestWhileIdle(testWhileIdle);
datasource.setTestOnBorrow(testOnBorrow);
datasource.setTestOnReturn(testOnReturn);
datasource.setPoolPreparedStatements(poolPreparedStatements);
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
datasource.setFilters(filters);
} catch (SQLException e) {

}
datasource.setConnectionProperties(connectionProperties);
return datasource;
}
}
配置druid相关的sql监控
StatViewServlet

StatViewServlet是一个标准的javax.servlet.http.HttpServlet,使用时候需要注入

  • 根据配置中的url-pattern来访问内置监控页面,如果是上面的配置/druid/*,内置监控页面的首页是/druid/index.html

  • 配置allow和deny。deny优先于allow,如果在deny列表中,就算在allow列表中,也会被拒绝。如果allow没有配置或者为空,则允许所有访问

  • 配置resetEnable,在StatViewSerlvet输出的html页面中,有一个功能是Reset All,执行这个操作之后,会导致所有计数器清零,重新计数。你可以通过配置参数关闭它。

statFilter

statFilter用于采集web-jdbc关联监控的数据。

  • exlusions配置经常需要排除一些不必要的url,比如.js,/jslib/等等。配置在init-param中。
关键代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
public class DuridMonitorConfiguration {
@Bean
public ServletRegistrationBean statViewServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
servletRegistrationBean.addInitParameter("allow", "127.0.0.1"); //设置ip白名单
servletRegistrationBean.addInitParameter("deny", "192.168.0.19");//设置ip黑名单,优先级高于白名单
//设置控制台管理用户
servletRegistrationBean.addInitParameter("loginUsername", "root");
servletRegistrationBean.addInitParameter("loginPassword", "root");
//是否可以重置数据
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}

@Bean
public FilterRegistrationBean statFilter() {
//创建过滤器
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
//设置过滤器过滤路径
filterRegistrationBean.addUrlPatterns("/*");
//忽略过滤的形式
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
测试
配置log4j

开启应用,发现出错了显示,Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Priority,这是由于druid打印sql语句时候依赖于log4j,所以导入log4j依赖包:

1
2
3
4
5
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
访问监控页面

启动后直接访问地址 http://localhost:8080/druid/

测试数据库连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {DemoApplication.class})
public class DemoTest {
@Autowired
private DataSource dataSource;


@Test
public void test() throws SQLException {
System.out.println("测试数据库");
testDataSource(dataSource);
}

public void testDataSource(DataSource dataSource) throws SQLException {
Connection connection = dataSource.getConnection();
CallableStatement callableStatement = connection.prepareCall("SELECT * FROM test");
ResultSet resultSet = callableStatement.executeQuery();
while (resultSet.next()) {
String value = resultSet.getString("name");
System.out.println(value);
System.out.println("===========");
}
resultSet.close();
connection.close();
}
}

打印数据

1
2
3
测试数据库
test1数据库
===========

多数据源配置

数据准备阶段

先在数据库中创建好test1库和test2库,然后分别插入数据

数据源配置
属性文件配置

在application.properties中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 数据库访问配置
# 主数据源
spring.datasource.primary.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.primary.username=root
spring.datasource.primary.password=root
spring.datasource.primary.driverClassName=com.mysql.jdbc.Driver
#从数据源
spring.datasource.second.url=jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.second.username=root
spring.datasource.second.password=root
spring.datasource.second.driverClassName=com.mysql.jdbc.Driver

######################### Druid连接池的配置信息 #################
#初始化连接大小
spring.druid.initialSize=5
#最小连接池数量
spring.druid.minIdle=5
#最大连接池数量
spring.druid.maxActive=20
#获取连接时最大等待时间,单位毫秒
spring.druid.maxWait=60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.druid.timeBetweenEvictionRunsMillis=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
spring.druid.minEvictableIdleTimeMillis=300000
#测试连接
spring.druid.validationQuery=SELECT 1 FROM DUAL
#申请连接的时候检测,建议配置为true,不影响性能,并且保证安全性
spring.druid.testWhileIdle=true
#获取连接时执行检测,建议关闭,影响性能
spring.druid.testOnBorrow=false
#归还连接时执行检测,建议关闭,影响性能
spring.druid.testOnReturn=false
#是否开启PSCache,PSCache对支持游标的数据库性能提升巨大,oracle建议开启,mysql下建议关闭
spring.druid.poolPreparedStatements=false
#开启poolPreparedStatements后生效
spring.druid.maxPoolPreparedStatementPerConnectionSize=20
#配置扩展插件,常用的插件有=>stat:监控统计 log4j:日志 wall:防御sql注入
spring.druid.filters=stat,wall,log4j
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.druid.connectionProperties='druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000'
配置类配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Configuration
public class DruidConfiguration {
@Value("${spring.druid.initialSize}")
private int initialSize;

@Value("${spring.druid.minIdle}")
private int minIdle;

@Value("${spring.druid.maxActive}")
private int maxActive;

@Value("${spring.druid.maxWait}")
private int maxWait;

@Value("${spring.druid.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;

@Value("${spring.druid.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;

@Value("${spring.druid.validationQuery}")
private String validationQuery;

@Value("${spring.druid.testWhileIdle}")
private boolean testWhileIdle;

@Value("${spring.druid.testOnBorrow}")
private boolean testOnBorrow;

@Value("${spring.druid.testOnReturn}")
private boolean testOnReturn;

@Value("${spring.druid.poolPreparedStatements}")
private boolean poolPreparedStatements;

@Value("${spring.druid.maxPoolPreparedStatementPerConnectionSize}")
private int maxPoolPreparedStatementPerConnectionSize;

@Value("${spring.druid.filters}")
private String filters;

@Value("{spring.druid.connectionProperties}")
private String connectionProperties;

public DataSource dataSource() {
DruidDataSource datasource = new DruidDataSource();

//configuration
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setValidationQuery(validationQuery);
datasource.setTestWhileIdle(testWhileIdle);
datasource.setTestOnBorrow(testOnBorrow);
datasource.setTestOnReturn(testOnReturn);
datasource.setPoolPreparedStatements(poolPreparedStatements);
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
datasource.setFilters(filters);
} catch (SQLException e) {

}
datasource.setConnectionProperties(connectionProperties);
return datasource;
}

@Bean(name = "primaryDatasource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
@Primary
public DataSource primaryDatasource() {
return dataSource();
}

@Bean(name = "secondDatasource")
@ConfigurationProperties(prefix = "spring.datasource.second")
public DataSource secondDatasource() {
return dataSource();
}
}
测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {DemoApplication.class})
public class DemoTest {
@Resource(name="primaryDatasource")
private DataSource primaryDatasource;
@Resource(name="secondDatasource")
private DataSource secondDatasource;

@Test
public void test() throws SQLException {
System.out.println("测试数据库1");
testDataSource(primaryDatasource);
System.out.println("测试数据库12");
testDataSource(secondDatasource);
}

public void testDataSource(DataSource dataSource) throws SQLException {
Connection connection = dataSource.getConnection();
CallableStatement callableStatement = connection.prepareCall("SELECT * FROM test");
ResultSet resultSet = callableStatement.executeQuery();
while (resultSet.next()) {
String value = resultSet.getString("name");
System.out.println(value);
System.out.println("===========");
}
resultSet.close();
connection.close();
}
}

打印数据

1
2
3
4
5
6
测试数据库1
test1数据库
===========
测试数据库12
test2数据库
===========

整合 JdbcTemplate

在 Java 领域,数据持久化有几个常见的方案,有 Spring 自带的 JdbcTemplate 、有 MyBatis,还有 JPA,在这些方案中,最简单的就是 Spring 自带的 JdbcTemplate 了,这个东西虽然没有 MyBatis 那么方便,但是比起最开始的 Jdbc 已经强了很多了,它没有 MyBatis 功能那么强大,当然也意味着它的使用比较简单,事实上,JdbcTemplate 算是最简单的数据持久化方案了,本文就和大伙来说说这个东西的使用。

导入坐标

在pom文件中加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>

数据源配置

基于上面的但数据源或者多数据源配都可以

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {DemoApplication.class})
public class DemoTest {
@Autowired
private JdbcTemplate jdbcTemplate;


@Test
public void test() throws SQLException {
query();
insert();
update();
delete();
}

//查询
public void query() {
jdbcTemplate.query("SELECT * FROM test", new RowMapper() {
@Override
public Object mapRow(ResultSet resultSet, int i) throws SQLException {
String name = resultSet.getString("name");
System.out.println(name);
return name;
}
});
}
//插入
public void insert() {
jdbcTemplate.update("INSERT INTO test (NAME) VALUES (?)", "插入操作");
}
// 更新
public void update() {
jdbcTemplate.update("update test set name =? where id = ?", 1, "xxxx");

}
//删除
public void delete() {
jdbcTemplate.update("delete from test where id = ?", 1);
}
}

原理分析

那么在 SpringBoot 中,配置完数据库基本信息之后,就有了一个 JdbcTemplate 了,这个东西是从哪里来的呢?源码在 org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration 类中,该类源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Configuration
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(JdbcProperties.class)
public class JdbcTemplateAutoConfiguration {
@Configuration
static class JdbcTemplateConfiguration {
private final DataSource dataSource;
private final JdbcProperties properties;
JdbcTemplateConfiguration(DataSource dataSource, JdbcProperties properties) {
this.dataSource = dataSource;
this.properties = properties;
}
@Bean
@Primary
@ConditionalOnMissingBean(JdbcOperations.class)
public JdbcTemplate jdbcTemplate() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSource);
JdbcProperties.Template template = this.properties.getTemplate();
jdbcTemplate.setFetchSize(template.getFetchSize());
jdbcTemplate.setMaxRows(template.getMaxRows());
if (template.getQueryTimeout() != null) {
jdbcTemplate
.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
}
return jdbcTemplate;
}
}
@Configuration
@Import(JdbcTemplateConfiguration.class)
static class NamedParameterJdbcTemplateConfiguration {
@Bean
@Primary
@ConditionalOnSingleCandidate(JdbcTemplate.class)
@ConditionalOnMissingBean(NamedParameterJdbcOperations.class)
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(
JdbcTemplate jdbcTemplate) {
return new NamedParameterJdbcTemplate(jdbcTemplate);
}
}
}

​ 从这个类中,大致可以看出,当前类路径下存在 DataSource 和 JdbcTemplate 时,该类就会被自动配置,jdbcTemplate 方法则表示,如果开发者没有自己提供一个 JdbcOperations 的实例的话,系统就自动配置一个 JdbcTemplate Bean(JdbcTemplate 是 JdbcOperations 接口的一个实现)。

整合多数据源

创建 JdbcTemplateConfig 类,用来提供两个不同的 JdbcTemplate 实例,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class JdbcTemplateConfig {

@Bean(name = "primaryJdbcTemplate")
JdbcTemplate jdbcTemplateOne(@Qualifier("primaryDatasource") DataSource primaryDatasource) {
return new JdbcTemplate(primaryDatasource);
}

@Bean(name = "secondJdbcTemplate")
JdbcTemplate jdbcTemplateTwo(@Qualifier("secondDatasource") DataSource secondDatasource) {
return new JdbcTemplate(secondDatasource);
}
}

每一个 JdbcTemplate 的创建都需要一个 DataSource,由于 Spring 容器中现在存在两个 DataSource,默认使用类型查找,会报错,因此加上 @Qualifier 注解,表示按照名称查找。这里创建了两个 JdbcTemplate 实例,分别对应了两个 DataSource。

接下来直接去使用这个 JdbcTemplate 就可以了。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {DemoApplication.class})
public class DemoTest {
@Resource(name = "primaryJdbcTemplate")
private JdbcTemplate primaryJdbcTemplate;

@Resource(name = "secondJdbcTemplate")
private JdbcTemplate secondJdbcTemplate;

@Test
public void test() throws SQLException {
query(primaryJdbcTemplate);
query(secondJdbcTemplate);
}

public void query(JdbcTemplate jdbcTemplate) {
jdbcTemplate.query("SELECT * FROM test", new RowMapper() {
@Override
public Object mapRow(ResultSet resultSet, int i) throws SQLException {
String name = resultSet.getString("name");
System.out.println(name);
return name;
}
});
}
}

和 DataSource 一样,Spring 容器中的 JdbcTemplate 也是有两个,因此不能通过 byType 的方式注入进来,这里给大伙提供了两种注入思路,一种是使用 @Resource 注解,直接通过 byName 的方式注入进来,另外一种就是 @Autowired 注解加上 @Qualifier 注解,两者联合起来,实际上也是 byName。将 JdbcTemplate 注入进来之后,jdbcTemplateOne 和 jdbcTemplateTwo 此时就代表操作不同的数据源,使用不同的 JdbcTemplate 操作不同的数据源,实现了多数据源配置。

整合 MyBatis

Spring Boot 中最简单的数据持久化方案 JdbcTemplate,JdbcTemplate 虽然简单,但是用的并不多,因为它没有 MyBatis 方便,在 Spring+SpringMVC 中整合 MyBatis 步骤还是有点复杂的,要配置多个 Bean,Spring Boot 中对此做了进一步的简化,使 MyBatis 基本上可以做到开箱即用,本文就来看看在 Spring Boot 中 MyBatis 要如何使用。

导入坐标

在pom文件中加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>

数据源配置

基于上面的但数据源或者多数据源配都可以

注解方式使用

创建实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Entity {

private Integer id;

private String name;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "Entity{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
注解方式使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface TestMapper {

@Select("select * from test")
List<Entity> getAllDatas();

@Results({
@Result(property = "id", column = "id"),
@Result(property = "name", column = "name")
})
@Select("select * from test where id=#{id}")
Entity getById(Long id);

@Select("select * from test where name like concat('%',#{name},'%')")
List<Entity> getByName(String name);

@Insert({"insert into test(name) values(#{name})"})
@SelectKey(statement = "select last_insert_id()", keyProperty = "id", before = false, resultType = Integer.class)
Integer addUser(Entity entity);

@Update("update test set name=#{name} where id=#{id}")
Integer updateById(Entity entity);

@Delete("delete from test where id=#{id}")
Integer deleteById(Integer id);

}

这里是通过全注解的方式来写 SQL,不写 XML 文件。

@Select、@Insert、@Update 以及 @Delete 四个注解分别对应 XML 中的 select、insert、update 以及 delete 标签,@Results 注解类似于 XML 中的 ResultMap 映射文件(getUserById 方法给查询结果的字段取别名主要是向小伙伴们演示下 @Results 注解的用法)。

另外使用 @SelectKey 注解可以实现主键回填的功能,即当数据插入成功后,插入成功的数据 id 会赋值到 user 对象的id 属性上。

配置启动类

TestMapper 创建好之后,还要配置 mapper 扫描,有两种方式,一种是直接在 TestMapper 上面添加 @Mapper 注解,这种方式有一个弊端就是所有的 Mapper 都要手动添加,要是落下一个就会报错,还有一个一劳永逸的办法就是直接在启动类上添加 Mapper 扫描,如下:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@MapperScan(basePackages = "com.test.demo.mapper")
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

好了,做完这些工作就可以去测试 Mapper 的使用了。

测试
1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(value = SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {DemoApplication.class})
public class DemoTest {

@Autowired
private TestMapper TestMapper;
@Test
public void test() throws SQLException {
List<Entity> dataList = TestMapper.getAllDatas();
System.out.println(dataList);
}
}

配置方式使用

mapper 接口

当然,开发者也可以在 XML 中写 SQL,例如创建一个 TestMapper,如下:

1
2
3
4
5
6
7
8
9
10
11
public interface TestMapper1 {

List<Entity> getAllDatas();

Integer addEntity(Entity entity);

Integer updateById(Entity entity);

Integer deleteById(Integer id);

}
mapper 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.test.demo.mapper.TestMapper">

<resultMap type="com.test.demo.entity.Entity" id="entity">
<id property="id" column="id"/>
<result property="name" column="name"/>
</resultMap>


<select id="getAllDatas" parameterType="com.test.demo.entity.Entity" resultMap="entity">
select * from test
</select>
<insert id="addEntity" parameterType="com.test.demo.entity.Entity">
insert into test(name) values(#{name})
</insert>
<update id="updateById" parameterType="com.test.demo.entity.Entity">
update user set username=#{username},address=#{address} where id=#{id}
</update>
<delete id="deleteById" parameterType="int">
delete from user where id=#{id}
</delete>
</mapper>

将接口中方法对应的 SQL 直接写在 XML 文件中。

POM文件配置XML编译

java 目录下的 xml 资源在项目打包时会被忽略掉,所以,如果 TestMapper.xml 放在包下,需要在 pom.xml 文件中再添加如下配置,避免打包时 java 目录下的 XML 文件被自动忽略掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>
配置application.properties

此时在 application.properties 中告诉 mybatis 去哪里扫描 mapper:

1
mybatis.mapper-locations=classpath:mapper/*.xml

如此配置之后,mapper 就可以正常使用了。注意这种方式不需要在 pom.xml 文件中配置文件过滤。

原理分析

在 SSM 整合中,开发者需要自己提供两个 Bean,一个SqlSessionFactoryBean ,还有一个是 MapperScannerConfigurer,在 Spring Boot 中,这两个东西虽然不用开发者自己提供了,但是并不意味着这两个 Bean 不需要了,在 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 类中,我们可以看到 Spring Boot 提供了这两个 Bean,部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration implements InitializingBean {

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
@org.springframework.context.annotation.Configuration
@Import({ AutoConfiguredMapperScannerRegistrar.class })
@ConditionalOnMissingBean(MapperFactoryBean.class)
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

@Override
public void afterPropertiesSet() {
logger.debug("No {} found.", MapperFactoryBean.class.getName());
}
}
}

从类上的注解可以看出,当当前类路径下存在 SqlSessionFactory、 SqlSessionFactoryBean 以及 DataSource 时,这里的配置才会生效,SqlSessionFactory 和 SqlTemplate 都被提供了。

动态源配置

在很多具体应用场景中,我们需要用到动态数据源的情况,比如多租户的场景,系统登录时需要根据用户信息切换到用户对应的数据库。又比如业务A要访问A数据库,业务B要访问B数据库等,都可以使用动态数据源方案进行解决。接下来,我们就来讲解如何实现动态数据源,以及在过程中剖析动态数据源背后的实现原理。

数据源配置类

创建一个数据源配置类,主要做以下几件事情:

  1. 配置 dao,model,xml mapper文件的扫描路径。

  2. 注入数据源配置属性,创建master、slave数据源。

  3. 创建一个动态数据源,并装入master、slave数据源。

  4. 将动态数据源设置到SQL会话工厂和事务管理器。

动态数据源相关配置

当进行数据库操作时,就会通过我们创建的动态数据源去获取要操作的数据源了

数据源还是使用的原来配置的durid的多数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Configuration
public class MybatisConfig {

@Resource(name = "primaryDatasource")
private DataSource master;
@Resource(name = "secondDatasource")
private DataSource slave;

@Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(DynamicDataSourceContextHolder.MASTER_DB, master);
dataSourceMap.put(DynamicDataSourceContextHolder.SLAVE_DB, slave);
// 将 master 数据源作为默认指定的数据源
dynamicDataSource.setDefaultDataSource(master);
// 将 master 和 slave 数据源作为指定的数据源
dynamicDataSource.setDataSources(dataSourceMap);
return dynamicDataSource;
}

@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
// 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource作为数据源则不能实现切换
sessionFactory.setDataSource(dynamicDataSource());
sessionFactory.setTypeAliasesPackage("com.test.demo.entity"); // 扫描Model
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml")); // 扫描映射文件
return sessionFactory;
}

@Bean
public PlatformTransactionManager transactionManager() {
// 配置事务管理, 使用事务时在方法头部添加@Transactional注解即可
return new DataSourceTransactionManager(dynamicDataSource());
}
}

动态数据源类

我们上一步把这个动态数据源设置到了SQL会话工厂和事务管理器,这样在操作数据库时就会通过动态数据源类来获取要操作的数据源了。

​ 动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource 中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key。

​ 所以如果我们需要动态切换数据源,就可以通过以下两种方式定制:

  1. 覆写 determineCurrentLookupKey 方法

    ​ 通过覆写 determineCurrentLookupKey 方法,从一个自定义的 DynamicDataSourceContextHolder.getDataSourceKey() 获取数据源key值,这样在我们想动态切换数据源的时候,只要通过 DynamicDataSourceContextHolder.setDataSourceKey(key) 的方式就可以动态改变数据源了。这种方式要求在获取数据源之前,要先初始化各个数据源到 DynamicDataSource 中,我们案例就是采用这种方式实现的,所以在 MybatisConfig 中把master和slave数据源都事先初始化到DynamicDataSource 中。

  2. 覆写 determineTargetDataSource

    ​ 因为数据源就是在这个方法创建并返回的,所以这种方式就比较自由了,支持到任何你希望的地方读取数据源信息,只要最终返回一个 DataSource 的实现类即可。比如你可以到数据库、本地文件、网络接口等方式读取到数据源信息然后返回相应的数据源对象就可以了。

DynamicDataSource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 动态数据源实现类
*/
public class DynamicDataSource extends AbstractRoutingDataSource {


/**
* 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
* 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
*/
@Override
protected DataSource determineTargetDataSource() {
return super.determineTargetDataSource();
}

/**
* 如果希望所有数据源在启动配置时就加载好,这里通过设置数据源Key值来切换数据,定制这个方法
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}

/**
* 设置默认数据源
*
* @param defaultDataSource
*/
public void setDefaultDataSource(Object defaultDataSource) {
super.setDefaultTargetDataSource(defaultDataSource);
}

/**
* 设置数据源
*
* @param dataSources
*/
public void setDataSources(Map<Object, Object> dataSources) {
super.setTargetDataSources(dataSources);
// 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
}
}
数据源上下文

动态数据源的切换主要是通过调用这个类的方法来完成的。在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。比如系统登录时,根据用户信息调用这个类的数据源切换方法切换到用户对应的数据库。

主要方法介绍

切换数据源

在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。

1
2
3
4
5
6
7
/**
* 切换数据源
* @param key
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}

重置数据源

将数据源重置回默认的数据源。默认数据源通过 DynamicDataSource.setDefaultDataSource(ds) 进行设置。

1
2
3
4
5
6
/**
* 重置数据源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
DynamicDataSourceContextHolder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* 动态数据源上下文
*/
public class DynamicDataSourceContextHolder {
public static final String MASTER_DB = "master";
public static final String SLAVE_DB = "slave";

private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/**
* 将 master 数据源的 key作为默认数据源的 key
*/
@Override
protected String initialValue() {
return MASTER_DB;
}
};


/**
* 数据源的 key集合,用于切换时判断数据源是否存在
*/
public static List<Object> dataSourceKeys = new ArrayList<>();

/**
* 切换数据源
*
* @param key
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}

/**
* 获取数据源
*
* @return
*/
public static String getDataSourceKey() {
return contextHolder.get();
}

/**
* 重置数据源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}

/**
* 判断是否包含数据源
*
* @param key 数据源key
* @return
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}

/**
* 添加数据源keys
*
* @param keys
* @return
*/
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}

注解式数据源

到这里,在任何想要动态切换数据源的时候,只要调用 DynamicDataSourceContextHolder.setDataSourceKey(key) 就可以完成了。

POM配置AOP

因为要使用注解的方式所有需要导入aop支持

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建数据源注解

接下来我们实现通过注解的方式来进行数据源的切换,原理就是添加注解(如@DataSource(value=”master”)),然后实现注解切面进行数据源切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 动态数据源注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

/**
* 数据源key值 默认master
*
* @return
*/
String value() default DynamicDataSourceContextHolder.MASTER_DB;

}
创建AOP切面

创建一个AOP切面,拦截带 @DataSource 注解的方法,在方法执行前切换至目标数据源,执行完成后恢复到默认数据源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 动态数据源注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

/**
* 数据源key值 默认master
*
* @return
*/
String value() default DynamicDataSourceContextHolder.MASTER_DB;
}

评论