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 { @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; } @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; } @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 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 spring.druid.testWhileIdle =true spring.druid.testOnBorrow =false spring.druid.testOnReturn =false spring.druid.poolPreparedStatements =false spring.druid.maxPoolPreparedStatementPerConnectionSize =20 spring.druid.filters =stat,wall,log4j 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); 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" ); servletRegistrationBean.addInitParameter("deny" , "192.168.0.19" ); 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 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 spring.druid.testWhileIdle =true spring.druid.testOnBorrow =false spring.druid.testOnReturn =false spring.druid.poolPreparedStatements =false spring.druid.maxPoolPreparedStatementPerConnectionSize =20 spring.druid.filters =stat,wall,log4j 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 (); 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数据库等,都可以使用动态数据源方案进行解决。接下来,我们就来讲解如何实现动态数据源,以及在过程中剖析动态数据源背后的实现原理。
数据源配置类
创建一个数据源配置类,主要做以下几件事情:
配置 dao,model,xml mapper文件的扫描路径。
注入数据源配置属性,创建master、slave数据源。
创建一个动态数据源,并装入master、slave数据源。
将动态数据源设置到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); dynamicDataSource.setDefaultDataSource(master); dynamicDataSource.setDataSources(dataSourceMap); return dynamicDataSource; } @Bean public SqlSessionFactoryBean sqlSessionFactoryBean () throws Exception { SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean (); sessionFactory.setDataSource(dynamicDataSource()); sessionFactory.setTypeAliasesPackage("com.test.demo.entity" ); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver (); sessionFactory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml" )); return sessionFactory; } @Bean public PlatformTransactionManager transactionManager () { return new DataSourceTransactionManager (dynamicDataSource()); } }
动态数据源类
我们上一步把这个动态数据源设置到了SQL会话工厂和事务管理器,这样在操作数据库时就会通过动态数据源类来获取要操作的数据源了。
动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource 中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key。
所以如果我们需要动态切换数据源,就可以通过以下两种方式定制:
覆写 determineCurrentLookupKey 方法
通过覆写 determineCurrentLookupKey 方法,从一个自定义的 DynamicDataSourceContextHolder.getDataSourceKey() 获取数据源key值,这样在我们想动态切换数据源的时候,只要通过 DynamicDataSourceContextHolder.setDataSourceKey(key) 的方式就可以动态改变数据源了。这种方式要求在获取数据源之前,要先初始化各个数据源到 DynamicDataSource 中,我们案例就是采用这种方式实现的,所以在 MybatisConfig 中把master和slave数据源都事先初始化到DynamicDataSource 中。
覆写 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 { @Override protected DataSource determineTargetDataSource () { return super .determineTargetDataSource(); } @Override protected Object determineCurrentLookupKey () { return DynamicDataSourceContextHolder.getDataSourceKey(); } public void setDefaultDataSource (Object defaultDataSource) { super .setDefaultTargetDataSource(defaultDataSource); } public void setDataSources (Map<Object, Object> dataSources) { super .setTargetDataSources(dataSources); DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet()); } }
数据源上下文
动态数据源的切换主要是通过调用这个类的方法来完成的。在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。比如系统登录时,根据用户信息调用这个类的数据源切换方法切换到用户对应的数据库。
主要方法介绍 切换数据源
在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。
1 2 3 4 5 6 7 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>() { @Override protected String initialValue () { return MASTER_DB; } }; public static List<Object> dataSourceKeys = new ArrayList <>(); public static void setDataSourceKey (String key) { contextHolder.set(key); } public static String getDataSourceKey () { return contextHolder.get(); } public static void clearDataSourceKey () { contextHolder.remove(); } public static boolean containDataSourceKey (String key) { return dataSourceKeys.contains(key); } 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 { 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 { String value () default DynamicDataSourceContextHolder.MASTER_DB; }