梳理 Spring Boot 默认配置的数据库和连接池,以及自定义配置的方法
Spring Boot gives you defaults on all things. For example, the default database is H2.
Consequently, when you want to use any other database, you must define the connection attributes in the application.properties file.
# org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection#H2
# 内嵌数据库支持 H2 Derby Hsqldb,首选 H2。
spring.datasource.url=jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/db_example
spring.datasource.username=springuser
spring.datasource.password=ThePassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.jpa.show-sql: true
/**
* 查找最匹配的 EmbeddedDatabaseConnection
* Spring Boot 内置EmbeddedDatabaseConnection 配置。作为 DataSourceProperties 托底配置。
* 如果没有指定的DatabaseConnection,就会取 EmbeddedDatabaseConnection。
* @see org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
* @see org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection
*/
public static EmbeddedDatabaseConnection get(ClassLoader classLoader) {
if (override != null) {
return override;
}
// 按照定义顺序,遍历 H2 Derby Hsqldb
for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) {
// 如果存在数据库 DriverClass,就返回对应数据库的 Connection
if (candidate != NONE && ClassUtils.isPresent(candidate.getDriverClassName(), classLoader)) {
return candidate;
}
}
return NONE;
}
pagehelper/pagehelper-spring-boot
auto-configure class
META-INF/spring.factories
org.springframework.context.annotation.Configuration
org.springframework.boot.context.properties.EnableConfigurationProperties
①对照上述的关键要素,看是否有缺失
②检查是否有加载 spring.factories
org.springframework.core.io.support.SpringFactoriesLoader
③开启 DEBUG 日志,Spring Boot 会输出 AUTO-CONFIGURATION REPORT 根据 Negative matches
判断原因。
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportMessage
在使用 Spring Boot 配置工程时,发现并没有按照预期解析。疑问:是yaml 的解析规则还是 Spring Boot 解析规则?
# yaml 配置
alias:
52: s52-online
# springboot 解析完成的key-value
# alias[52] -> s-52-online
/**
* YamlProcessor to create a Map containing the property values.
* @see org.springframework.boot.env.YamlPropertySourceLoader.Processor#process
*/
public Map<String, Object> process() {
final Map<String, Object> result = new LinkedHashMap<String, Object>();
process(new MatchCallback() {
// loadYaml完成后的object 强制转换为 map数据结构
public void process(Properties properties, Map<String, Object> map) {
// 拍平map数据结构,最终得到的是 property values
result.putAll(getFlattenedMap(map));
}
});
return result;
}
首先,processor 会针对yaml 读取完的结果进行处理,主要就是把number keys 转为string keys。
// 得到的数据:{alias={[52]=s-52-online}}
private Map<String, Object> asMap(Object object) {
// YAML can have numbers as keys
for (Entry<Object, Object> entry : map.entrySet()) {
Object key = entry.getKey();
if (key instanceof CharSequence) {
result.put(key.toString(), value);
}
else {
// It has to be a map key in this case
result.put("[" + key.toString() + "]", value);
}
}
return result;
}
然后,再把map 拍平,得到最终的the property values。
private void buildFlattenedMap(Map<String, Object> source, String path) {
for (Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
if (StringUtils.hasText(path)) {
// 针对上述number keys的key,直接追加到path,得到的就是 alias[52]
if (key.startsWith("[")) {
key = path + key;
}
else {
key = path + '.' + key;
}
}
// ... value 拍平处理,理论上只有String, Map, Collection
}
}
yaml 是一种文件格式的规范,YAML Version 1.1。
java 读取yaml使用到的第三方库:snakeyaml。SnakeYAML Engine Documentation。
snakeyaml 是根据YAML规范实现的。
Representation Graph相关:
Nodes: YAML nodes have content of one of three kinds: scalar, sequence, or mapping.
处理相关:
Parse : Parsing is the inverse process of presentation, it takes a stream of characters and produces a series of events.
Compose : Composing takes a series of serialization events and produces a representation graph.
Construct : The final input process is constructing native data structures from the YAML representation.
// yaml load file
new Composer(new ParserImpl(new StreamReader(new UnicodeReader(input))), new Resolver());
第三方库实现针对规范中的定义进行了具体的实现,对功能进行了清晰明确的划分。
最近排查线上问题发现业务 Table 自增id 不连续,进一步发现是事务回滚造成。
-- 上述问题示例。 这样的情况就导致 main_table 自增id 并不连续/出现空挡。
START TRANSACTION;
-- 执行成功,自增id 已分配
INSERT INTO main_table VALUES (?, ?, ?);
-- 执行失败,事务 rollback
INSERT INTO sub_table VALUES (?, ?, ?);
那么,自增id 在数据库中是什么样的机制?可以从 H2 数据库实现方案获得启发。
-- h2 auto_increment 建表语句
create table test(id bigint auto_increment, name varchar(255));
insert into test(name) values('hello');
insert into test(name) values('world');
select * from test;
通过打开 H2 数据库文件,可以看到 test 建表语句是按照H2 dalect 翻译的。可以看到H2 数据库的auto_increment 是通过 SEQUENC 实现的。
CREATE CACHED TABLE PUBLIC.TEST(
ID BIGINT DEFAULT (NEXT VALUE FOR PUBLIC.SYSTEM_SEQUENCE_620D39CD) NOT NULL NULL_TO_DEFAULT SEQUENCE PUBLIC.SYSTEM_SEQUENCE_620D39CD,
NAME VARCHAR(255)
)
官方文档 create_sequence ✨
-- Sequence can produce only integer values.
CREATE SEQUENCE SEQ_ID;
CREATE SEQUENCE SEQ2 AS INTEGER START WITH 10;
Used values are never re-used, even when the transaction is rolled back.
org.h2.command.dml.Insert
org.h2.expression.SequenceValue
org.h2.schema.Sequence
creat table 实例化过程
// @see org.h2.command.ddl.CreateTable#update
for (Column c :data.columns) {
// 如果列定义为 auto_increment, 需要进行dialet 翻译。
if (c.isAutoIncrement()) {
int objId = getObjectId();
//
c.convertAutoIncrementToSequence(session, getSchema(), objId, data.temporary);
}
}
/**
* Convert the auto-increment flag to a sequence that is linked with this
* table.
*
* @see org.h2.table.Column#convertAutoIncrementToSequence
*/
public void convertAutoIncrementToSequence(Session session, Schema schema, int id, boolean temporary) {
// 生成唯一的 sequenceName
while (true) {
ValueUuid uuid = ValueUuid.getNewRandom();
String s = uuid.getString();
s = s.replace('-', '_').toUpperCase();
sequenceName = "SYSTEM_SEQUENCE_" + s;
if (schema.findSequence(sequenceName) == null) {
break;
}
}
// Sequence 实例化,实现自增生成唯一数据
Sequence seq = new Sequence(schema, id, sequenceName, start, increment);
if (temporary) {
seq.setTemporary(true);
} else {
// CREATE SEQUENCE, 数据库 Schema 持久化
session.getDatabase().addSchemaObject(session, seq);
}
setAutoIncrement(false, 0, 0);
// 把Sequence 包装为表达式片段
SequenceValue seqValue = new SequenceValue(seq);
setDefaultExpression(session, seqValue);
setSequence(seq);
}
Insert 过程
// 遍历 Table 的 columns 和 参数Expression, 执行值填充
for (int i = 0; i < columnLen; i++) {
Column c = columns[i];
int index = c.getColumnId();
// 获取 Value Expression
Expression e = expr[i];
if (e != null) {
try {
// 如果是SequenceValue, 生成自增数字
Value v = c.convert(e.getValue(session));
newRow.setValue(index, v);
} catch (DbException ex) {
throw setRow(ex, x, getSQL(expr));
}
}
}
/**
* Get the next value for this sequence.
*
* @param session the session
* @return the next value
*/
public synchronized long getNext(Session session) {
// valueWithMargin 计算, flush 过程...
long v = value;
value += increment;
return v;
}
H2 database 是java 编写的数据库,简单易懂,对于数据库实现原理是个很好参考。
通过阅读数据库的实现,对于应用开发帮助很大,可以适当的扬长避短。
对于SQL 规范,每种数据库都有对应的实现方式,dalect。
分页是 web application 开发最常见的功能。在使用不同的框架和工具过程中,发现初始行/页的定义不同,特意整理记录。
语法:[LIMIT {[offset,] row_count}]
LIMIT row_count
is equivalent to LIMIT 0, row_count
.
The offset of the initial row is 0 (not 1)
参考:MySQL :: MySQL 5.7 Reference Manual :: 13.2.9 SELECT Statement
后端分页,简单讲,就是数据库的分页。 对于mysql 来讲,就是上述 offset row_count 的计算过程。
/**
* 计算起止行号 offset
* @see com.github.pagehelper.Page#calculateStartAndEndRow
*/
private void calculateStartAndEndRow() {
// pageNum 页码,从1开始。 pageNum < 1 , 忽略计算。
this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
}
/**
* 计算总页数 pages/ pageCount。
*/
public void setTotal(long total) {
if (pageSize > 0) {
pages = (int) (total / pageSize + ((total % pageSize == 0) ? 0 : 1));
} else {
pages = 0;
}
}
SQL 拼接实现: com.github.pagehelper.dialect.helper.MySqlDialect
关键类:
org.springframework.data.domain.Pageable
org.springframework.data.web.PageableDefault
/**
* offset 计算,不同于pagehelper, page 页码从0 开始。 default is 0
* @see org.springframework.data.domain.AbstractPageRequest#getOffset
*/
public long getOffset() {
return (long)this.page * (long)this.size;
}
/*
* 使用 Math.ceil 实现。
* @see org.springframework.data.domain.Page#getTotalPages()
*/
@Override
public int getTotalPages() {
return getSize() == 0 ? 1 : (int) Math.ceil((double) total / (double) getSize());
}
/**
* offset 计算,不同于pagehelper, page 页码从0 开始。
* @see org.springframework.data.jdbc.core.convert.SqlGenerator#applyPagination
*/
private SelectBuilder.SelectOrdered applyPagination(Pageable pageable, SelectBuilder.SelectOrdered select) {
// 在spring-data-relation, Limit 抽象为 SelectLimitOffset
SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) select;
// To read the first 20 rows from start use limitOffset(20, 0). to read the next 20 use limitOffset(20, 20).
SelectBuilder.SelectLimitOffset limitResult = limitable.limitOffset(pageable.getPageSize(), pageable.getOffset());
return (SelectBuilder.SelectOrdered) limitResult;
}
spring-data-commons 提供 mvc 层的分页参数处理器
/**
* Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller method.
*
* @see org.springframework.data.web.PageableDefault
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface PageableDefault {
/**
* The default-size the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding
* parameter defined in request (default is 10).
*/
int size() default 10;
/**
* The default-pagenumber the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding
* parameter defined in request (default is 0).
*/
int page() default 0;
}
MVC 参数处理器: org.springframework.data.web.PageableHandlerMethodArgumentResolver
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
<!-- spring-data-examples\web\example\src\main\resources\templates\users.html-->
<nav>
<!-- class样式 bootstrap 默认的分页用法-->
<ul class="pagination" th:with="total = ${users.totalPages}">
<li th:if="${users.hasPrevious()}">
<a th:href="@{/users(page=${users.previousPageable().pageNumber},size=${users.size})}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<!-- spring-data-examples 分页计算从0 开始, /users?page=0&size=10 -->
<!-- 生成页码列表 ①②③④ -->
<li th:each="page : ${#numbers.sequence(0, total - 1)}"><a th:href="@{/users(page=${page},size=${users.size})}" th:text="${page + 1}">1</a></li>
<li th:if="${users.hasNext()}">
<a th:href="@{/users(page=${users.nextPageable().pageNumber},size=${users.size})}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
// from node_modules\element-ui\packages\pagination\src\pagination.js
// page-count 总页数,total 和 page-count 设置任意一个就可以达到显示页码的功能;
computed: {
internalPageCount() {
if (typeof this.total === 'number') {
// 页数计算使用 Math.ceil
return Math.max(1, Math.ceil(this.total / this.internalPageSize));
} else if (typeof this.pageCount === 'number') {
return Math.max(1, this.pageCount);
}
return null;
}
},
/**
* 起始页计算。 page 页码从1 开始。
*/
getValidCurrentPage(value) {
value = parseInt(value, 10);
const havePageCount = typeof this.internalPageCount === 'number';
let resetValue;
if (!havePageCount) {
if (isNaN(value) || value < 1) resetValue = 1;
} else {
// 强制赋值起始值 1
if (value < 1) {
resetValue = 1;
} else if (value > this.internalPageCount) {
// 数据越界,强制拉回到PageCount
resetValue = this.internalPageCount;
}
}
if (resetValue === undefined && isNaN(value)) {
resetValue = 1;
} else if (resetValue === 0) {
resetValue = 1;
}
return resetValue === undefined ? value : resetValue;
},