作者还是一只小菜鸡,如果你对文章中的内容感到质疑或想与作者讨论,欢迎私信和留言
本人最近入职了一家电商saas公司,因为物流开发一直追求的是稳定,所以项目业务迭代正常,而技术迭代缓慢,很多项目依旧用的是 JDBC 来做持久化层面的开发,我是在使用的过程中出了不少糗,所以痛定思痛回头再来好好复习。
首先可以选择用 idea 构建一个 spring 项目,这里推荐勾选 lombok、web、jdbc、actuator、h2嵌入式数据库作为数据源,然后点击创建项目,当项目成功加载后,编写配置文件:
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.hikari.maximumPoolSize=5
spring.datasource.hikari.minimumIdle=5
spring.datasource.hikari.idleTimeout=600000
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.maxLifetime=1800000
以及在 application 启动类里编写:
@SpringApplication
@Slf4j
public class Application implements CommandLineRunner {
@Autowired
private DataSource dataSource;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
public void run(String... args) throws Exception {
show();
}
private void show() throws SqlException {
log.info(dataSource.toString());
Connection connection = dataSource.getConnection();
log.info(connection.toString());
connection.close();
}
}
当一切准备就绪之后,你就可以启动项目,可以看到控制台输出了你配置的数据源的打印信息:
2022-04-15 21:03:21.623 INFO 14184 --- [ main] com.example.demo.DemoApplication : HikariDataSource (HikariPool-1)
2022-04-15 21:03:21.623 INFO 14184 --- [ main] com.example.demo.DemoApplication : HikariProxyConnection@1842102517 wrapping conn0: url=jdbc:h2:mem:testdb user=SA
随后你可以访问:http://localhost:8080/actuator/beans
,你可以发现 spring 帮我们自动配置了很多类,那么在 jdbc springboot 通过自动配置注入了什么类呢?
数据源配置成功后,我们就可以去操纵数据了,此时我们还需要再来了解一下配置文件:
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=dpassword
第一行的配置是可选的,spring 会自动根据我们的 url 来为我们配置合适的数据库驱动,因为我这里勾选的是 h2 数据库,所以可以通过 spring.datasource.schema 与 spring.datasource.data 确定初始化 sql 文件:
# data.sql
INSERT INTO FOO (ID, BAR) VALUES (1, 'aaa');
INSERT INTO FOO (ID, BAR) VALUES (2, 'bbb');
# schema.sql
CREATE TABLE FOO (ID INT IDENTITY, BAR VARCHAR(64));
再然后就需要你在 Application
启动类中加入方法:
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void run(String... args) throws Exception {
showConnection();
showSql();
}
private void showSql() throws Exception {
jdbcTemplate.queryForList("SELECT * FROM FOO").forEach(row -> log.info(row.toString()));
}
简单的了解过单数据源后,就需要去了解一下多数据源是如何实现,因为我们在项目的数据量达到一定的规模后是需要进行拆库拆表的,这时候如何去做多数据源的连接就很重要,使用到多数据源的时候我们就需要注意到如何让系统判断使用到的是那个 datasource
,对应的数据源的事务、ORM等如何做到区分。
最简单的方式就是为其中的一个数据源加上 @Primary
注释,这样 springboot 就会以这个数据源为主而进行自动配置。
当然如果你觉得每个数据源对于项目来说都很重要,那么你就可以将DataSourceAutoConfiguration、DataSourceTransactionManagerAutoConfiguration、JdbcTemplateAutoConfiguration
都排除掉,然后通过编写配置文件+自己手动 bean 注入的方式来构建多个数据源:
management.endpoints.web.exposure.include=*
spring.output.ansi.enabled=ALWAYS
foo.datasource.url=jdbc:h2:mem:foo
foo.datasource.username=sa
foo.datasource.password=
bar.datasource.url=jdbc:h2:mem:bar
bar.datasource.username=sa
bar.datasource.password=
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
JdbcTemplateAutoConfiguration.class})
@Slf4j
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
@ConfigurationProperties("foo.datasource")
public DataSourceProperties fooDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource fooDataSource() {
DataSourceProperties dataSourceProperties = fooDataSourceProperties();
log.info("foo datasource: {}", dataSourceProperties.getUrl());
return dataSourceProperties.initializeDataSourceBuilder().build();
}
@Bean
@Resource
public PlatformTransactionManager fooTxManager(DataSource fooDataSource) {
return new DataSourceTransactionManager(fooDataSource);
}
@Bean
@ConfigurationProperties("bar.datasource")
public DataSourceProperties barDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource barDataSource() {
DataSourceProperties dataSourceProperties = barDataSourceProperties();
log.info("bar datasource: {}", dataSourceProperties.getUrl());
return dataSourceProperties.initializeDataSourceBuilder().build();
}
@Bean
@Resource
public PlatformTransactionManager barTxManager(DataSource barDataSource) {
return new DataSourceTransactionManager(barDataSource);
}
}
当项目启动之后,你不仅可以在控制台上看到自己注入的两个数据源,还可以去http://localhost:8080/actuator/beans
上看到对应的bean
Druid 数据源是阿里巴巴开源的数据源,Druid 数据源为监控而生,内置强大的监控功能、能够防止 sql 注入且监控功能不影响到性能。推荐浏览:Druid
简单使用demo——配置、拦截器等
spring.output.ansi.enabled=ALWAYS
spring.datasource.url=jdbc:h2:mem:foo
spring.datasource.username=sa
spring.datasource.password=n/z7PyA5cvcXvs8px8FVmBVpaRyNsvJb3X7YfS38DJrIg25EbZaZGvH4aHcnc97Om0islpCAPc3MqsGvsrxVJw==
spring.datasource.druid.initial-size=5
spring.datasource.druid.max-active=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.filters=conn,config,stat,slf4j
spring.datasource.druid.connection-properties=config.decrypt=true;config.decrypt.key=${public-key}
spring.datasource.druid.filter.config.enabled=true
spring.datasource.druid.test-on-borrow=true
spring.datasource.druid.test-on-return=true
spring.datasource.druid.test-while-idle=true
public-key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALS8ng1XvgHrdOgm4pxrnUdt3sXtu/E8My9KzX8sXlz+mXRZQCop7NVQLne25pXHtZoDYuMh3bzoGj6v5HvvAQ8CAwEAAQ==
// 尝试编写一个 druid 连接池的拦截器
@Slf4j
public class ConnectionLogFilter extends FilterEventAdapter {
@Override
public void connection_connectBefore(FilterChain chain, Properties info) {
log.info("before");
}
@Override
public void connection_connectAfter(ConnectionProxy connection) {
log.info("after");
}
}
// 启动类
@Slf4j
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {
@Autowired
private DataSource dataSource;
@Autowired
private JdbcTemplate jdbcTemplate;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
log.info(dataSource.toString());
log.info(jdbcTemplate.toString());
}
}
慢sql统计
一般来说使用到druid
,我们都可以以下方式抓到项目内所有的慢 sql :
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=100
当我们接通数据源之后,就需要我们通过 jdbc 去操纵数据,那么接下来就是熟悉的流程:导包 -> 编写配置文件 -> 开发 -> 校验。
导包仍然是对应的lombok、方便检视结果的web、jdbc、h2,编写好对应的 pom.xml 文件后就需要编写对应的配置文件:
spring.output.ansi.enabled=ALWAYS
# 使得 h2 支持浏览器访问
spring.h2.console.enabled=true
spring.h2.console.path=/h2
# 别忘了你的 h2 的sql配置文件
# data.sql
INSERT INTO FOO (BAR) VALUES ('aaa');
# schema.sql
CREATE TABLE FOO (ID INT IDENTITY, BAR VARCHAR(64));
// 普通操作
@Slf4j
@Repository
public class FooDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsert;
public void insertData() {
Arrays.asList("b", "c").forEach(bar -> {
jdbcTemplate.update("INSERT INTO FOO (BAR) VALUES (?)", bar);
});
HashMap<String, String> row = new HashMap<>();
row.put("BAR", "d");
Number id = simpleJdbcInsert.executeAndReturnKey(row);
log.info("ID of d: {}", id.longValue());
}
public void listData() {
log.info("Count: {}",
jdbcTemplate.queryForObject("SELECT COUNT(*) FROM FOO", Long.class));
List<String> list = jdbcTemplate.queryForList("SELECT BAR FROM FOO", String.class);
list.forEach(s -> log.info("Bar: {}", s));
List<Foo> fooList = jdbcTemplate.query("SELECT * FROM FOO", new RowMapper<Foo>() {
@Override
public Foo mapRow(ResultSet rs, int rowNum) throws SQLException {
return Foo.builder()
.id(rs.getLong(1))
.bar(rs.getString(2))
.build();
}
});
fooList.forEach(f -> log.info("Foo: {}", f));
}
}
// 批量操作
@Repository
public class BatchFooDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void batchInsert() {
jdbcTemplate.batchUpdate("INSERT INTO FOO (BAR) VALUES (?)",
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setString(1, "b-" + i);
}
@Override
public int getBatchSize() {
return 2;
}
});
List<Foo> list = new ArrayList<>();
list.add(Foo.builder().id(100L).bar("b-100").build());
list.add(Foo.builder().id(101L).bar("b-101").build());
namedParameterJdbcTemplate
.batchUpdate("INSERT INTO FOO (ID, BAR) VALUES (:id, :bar)",
SqlParameterSourceUtils.createBatch(list));
}
}
如果你想测试结果,可以在 Application 启动类中实现 CommandLineRunner
并且注入 Dao 类在 run
方法中调用测试
spring jdbc 会将常见的数据库异常转换为 DataAccessException
为基类的异常。一般我们直接操纵数据库而产生的错误都是一些错误码显示,例如2002、1045之类的,spring 将这些错误码(可能来自不同平台,像 mysql 或者 h2 之类的)分门归类并且解析到框架中:
# 你可以直接在框架源码内观察
org/springframework/jdbc/support/sql-error-codes.xml
Classpath 下的 sql-error-codes.xml
你甚至可以针对这些错误码做一些自己的定制,比如你希望在我的mysql的前做一层代理,代理有自己的异常与其他数据库不同,然后你希望可以抛出自己的异常类,你可以参考以下的实现:
// 导入包是老几样 web lombok h2 jdbc
// h2 配置自己的错误代码处理可以参考 spring 框架的
// 配置文件-sql-error-codes.xml 放到 resource 目录下
<beans>
<bean id="H2" class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="badSqlGrammarCodes">
<value>42000,42001,42101,42102,42111,42112,42121,42122,42132</value>
</property>
<property name="duplicateKeyCodes">
<value>23001,23505</value>
</property>
<property name="dataIntegrityViolationCodes">
<value>22001,22003,22012,22018,22025,23000,23002,23003,23502,23503,23506,23507,23513</value>
</property>
<property name="dataAccessResourceFailureCodes">
<value>90046,90100,90117,90121,90126</value>
</property>
<property name="cannotAcquireLockCodes">
<value>50200</value>
</property>
<property name="customTranslations">
<bean class="org.springframework.jdbc.support.CustomSQLErrorCodesTranslation">
<property name="errorCodes" value="23001,23505" />
<property name="exceptionClass"
value="geektime.spring.data.errorcodedemo.CustomDuplicatedKeyException" />
</bean>
</property>
</bean>
</beans>
// java实现
public class CustomDuplicatedKeyException extends DuplicateKeyException {
public CustomDuplicatedKeyException(String msg) {
super(msg);
}
public CustomDuplicatedKeyException(String msg, Throwable cause) {
super(msg, cause);
}
}