示例代碼和結果
先上demo
配置文件
mytest:
date: 08:00:00
date1: 09:00:00
date2: 10:00:00
date3: 11:00:00
date4: 12:00:00
date5: 13:00:00
Mytest配置類
package com.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author PrecedeForetime
* @project demo
* @datetime 20/9/16 16:52
*/
@Configuration
@ConfigurationProperties(prefix = "mytest")
public class MyTest {
private String date;
private String date1;
private String date2;
private String date3;
private String date4;
private String date5;
// getter setter omit....
}
測試啟動類
package com.controller;
import com.config.MyTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
/**
* @author PrecedeForetime
* @project demo
* @datetime 2019/3/19 17:12
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class DemoControllerTest {
@Autowired
private MyTest myTest;
@Test
public void test1() {
System.out.println(myTest.getDate());
System.out.println(myTest.getDate1());
System.out.println(myTest.getDate2());
System.out.println(myTest.getDate3());
System.out.println(myTest.getDate4());
System.out.println(myTest.getDate5());
}
}
輸出結果:
08:00:00
09:00:00
36000
39600
43200
46800
可以很明顯看到輸出結果中的date2~date4四個值的結果和配置文件中的值不一致,大致可以判斷出這個值是該時間對應的秒數.
解決方法
這個問題的解決辦法很簡單,在yml配置文件中將對應的值加上雙引號即可正常拿到,例10:00:00寫成"10:00:00".
或者改用application.properties這個格式的配置文件,不用加雙引號也能正常拿到對應的值.
Spring 解析YAML文件源碼解析
因為比較好奇導致這個問題的原因,所以翻了下YAML文件解析的源碼,找到了對應的原因,我的springboot版本是 v2.1.3.RELEASE,實際解析yml的包,也就是解析異常的依賴包為org.yaml:snakeyaml-1.23.jar,下面說下解析異常的具體原因
spring解析yml的類的調用棧大致如下
//初始化yaml文件解析類,也就是依次解析各個yaml配置文件,與本文沒有太大關系,都是前期准備工作
org.springframework.boot.env.YamlPropertySourceLoader#load()
org.springframework.boot.env.OriginTrackedYamlLoader#load()
org.springframework.beans.factory.config.YamlProcessor#process(org.springframework.beans.factory.config.YamlProcessor.MatchCallback)
// 從這里開始具體解析application.yml這個文件,源碼也從這里開始貼
org.springframework.beans.factory.config.YamlProcessor#process(org.springframework.beans.factory.config.YamlProcessor.MatchCallback, org.yaml.snakeyaml.Yaml, org.springframework.core.io.Resource)
org.yaml.snakeyaml.Yaml#loadAll(java.io.Reader)
org.yaml.snakeyaml.constructor.BaseConstructor#getData()
org.yaml.snakeyaml.composer.Composer#getNode()
org.yaml.snakeyaml.composer.Composer#composeNode()
org.yaml.snakeyaml.composer.Composer#composeScalarNode()
org.yaml.snakeyaml.resolver.Resolver#resolve()
org.springframework.beans.factory.config.YamlProcessor#process(MatchCallback,Yaml, Resource)
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
int count = 0;
try {
if (logger.isDebugEnabled()) {
logger.debug("Loading from YAML: " + resource);
}
Reader reader = new UnicodeReader(resource.getInputStream());
try {
//如果debug能看到這里的object對象已經是解析完的yaml配置文件的內容了,所以需要看yaml.loadAll(reader)方法
for (Object object : yaml.loadAll(reader)) {
if (object != null && process(asMap(object), callback)) {
count++;
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
break;
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
" from YAML resource: " + resource);
}
}
finally {
reader.close();
}
}
catch (IOException ex) {
handleProcessError(resource, ex);
}
return (count > 0);
}
org.yaml.snakeyaml.Yaml#loadAll(java.io.Reader)
//這個方法的作用就是把一個yaml配置文件的中的各個對象包裝成一個Iterator,實際上並沒有直接解析,真正調用解析是在上一步的for循環遍歷的時候,可以理解為懶加載
public Iterable<Object> loadAll(Reader yaml) {
Composer composer = new Composer(new ParserImpl(new StreamReader(yaml)), resolver);
constructor.setComposer(composer);
Iterator<Object> result = new Iterator<Object>() {
@Override
public boolean hasNext() {
return constructor.checkData();
}
@Override
public Object next() {
//核心方法,繼續往里走
return constructor.getData();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
return new YamlIterable(result);
}
private static class YamlIterable implements Iterable<Object> {
private Iterator<Object> iterator;
public YamlIterable(Iterator<Object> iterator) {
this.iterator = iterator;
}
@Override
public Iterator<Object> iterator() {
return iterator;
}
}
org.yaml.snakeyaml.constructor.BaseConstructor#getData()
/**
* Construct and return the next document
*
* @return constructed instance
*/
public Object getData() {
// Construct and return the next document.
//從名稱可以看出是校驗作用,直接忽略
composer.checkNode();
//解析方法,直接往里走
Node node = composer.getNode();
if (rootTag != null) {
node.setTag(rootTag);
}
//這里會把INT類型的10:00:00轉換為36000,先不用管,記住這個方法就行,最后會詳細講
return constructDocument(node);
}
org.yaml.snakeyaml.composer.Composer#getNode()
/**
* Reads and composes the next document.
*
* @return The root node of the document or <code>null</code> if no more
* documents are available.
*/
public Node getNode() {
// Drop the DOCUMENT-START event.
//event這個概念我沒有深究,我理解為是解析器在解析過程中的各種不同的狀態和正在解析的位置(類似於光標),
//比如開始解析一個大的層級或者解析完一個大的層級,實際的event還有很多中其他狀態,有興趣的可以自己看
parser.getEvent();
// Compose the root node.
//往里走
Node node = composeNode(null);
// Drop the DOCUMENT-END event.
parser.getEvent();
this.anchors.clear();
recursiveNodes.clear();
return node;
}
debug過程中node對象的toString值
valueNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:int, value=10:00:00)>> }{ key=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=date2)>;
value=<NodeTuple keyNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=date2)>;
valueNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=09:00:00)>> }{ key=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=date3)>;
value=<NodeTuple keyNode=<org.yaml.snakeyaml.nodes.ScalarNode (tag=tag:yaml.org,2002:str, value=date3)>;
這里只取了兩個有代表性的值,其余的都是和這兩個相同,為了方便閱讀就省略了,重點關注09:00:00和10:00:00這兩個值解析出來的差異即可,在這段xml中可以發現幾個關鍵的信息,這幾個信息對於后面的源碼解讀非常重要.
第一個那就是節點的類型是org.yaml.snakeyaml.nodes.ScalarNode,這個對於下一步的源碼可以直接找到composeScalarNode這個入口;
第二個是解析正常的09:00:00數據類型被解析為str,解析異常的10:00:00的數據類型被判斷為int,可以猜到后者解析異常的原因和數據類型有關;
第三個就是數據類型的表示類tag;
org.yaml.snakeyaml.composer.Composer#composeNode()
private Node composeNode(Node parent) {
if (parent != null) recursiveNodes.add(parent);
final Node node;
if (parser.checkEvent(Event.ID.Alias)) {
AliasEvent event = (AliasEvent) parser.getEvent();
String anchor = event.getAnchor();
if (!anchors.containsKey(anchor)) {
throw new ComposerException(null, null, "found undefined alias " + anchor,
event.getStartMark());
}
node = anchors.get(anchor);
if (recursiveNodes.remove(node)) {
node.setTwoStepsConstruction(true);
}
} else {
NodeEvent event = (NodeEvent) parser.peekEvent();
String anchor = event.getAnchor();
// the check for duplicate anchors has been removed (issue 174)
if (parser.checkEvent(Event.ID.Scalar)) {
//debug發現這個地方才是真正讀取配置文件中值的地方,當然也會讀取key,繼續往里走
node = composeScalarNode(anchor);
} else if (parser.checkEvent(Event.ID.SequenceStart)) {
node = composeSequenceNode(anchor);
} else {
node = composeMappingNode(anchor);
}
}
recursiveNodes.remove(parent);
return node;
}
org.yaml.snakeyaml.composer.Composer#composeScalarNode()
//從上文可以得知解析異常的原因是配置項的數據類型判斷錯誤,同時知道了tag類
protected Node composeScalarNode(String anchor) {
ScalarEvent ev = (ScalarEvent) parser.getEvent();
String tag = ev.getTag();
boolean resolved = false;
Tag nodeTag;
if (tag == null || tag.equals("!")) {
//這里就是判斷生成tag的地方,繼續往里走
nodeTag = resolver.resolve(NodeId.scalar, ev.getValue(),
ev.getImplicit().canOmitTagInPlainScalar());
resolved = true;
} else {
nodeTag = new Tag(tag);
}
Node node = new ScalarNode(nodeTag, resolved, ev.getValue(), ev.getStartMark(),
ev.getEndMark(), ev.getScalarStyle());
if (anchor != null) {
anchors.put(anchor, node);
}
return node;
}
org.yaml.snakeyaml.resolver.Resolver(這個類很關鍵,代碼也不多就全部都貼了)
/**
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.yaml.snakeyaml.resolver;
...
/**
* Resolver tries to detect a type by content (when the tag is implicit)
*/
public class Resolver {
//判斷是否為bool類型的正則
public static final Pattern BOOL = Pattern
.compile("^(?:yes|Yes|YES|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$");
/**
* The regular expression is taken from the 1.2 specification but '_'s are
* added to keep backwards compatibility
*/
//浮點數類型判斷正則
public static final Pattern FLOAT = Pattern
.compile("^([-+]?(\\.[0-9]+|[0-9_]+(\\.[0-9_]*)?)([eE][-+]?[0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");
public static final Pattern INT = Pattern
.compile("^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[0-9a-fA-F_]+|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$");
public static final Pattern MERGE = Pattern.compile("^(?:<<)$");
public static final Pattern NULL = Pattern.compile("^(?:~|null|Null|NULL| )$");
public static final Pattern EMPTY = Pattern.compile("^$");
public static final Pattern TIMESTAMP = Pattern
.compile("^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?(?:[Tt]|[ \t]+)[0-9][0-9]?:[0-9][0-9]:[0-9][0-9](?:\\.[0-9]*)?(?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$");
public static final Pattern VALUE = Pattern.compile("^(?:=)$");
public static final Pattern YAML = Pattern.compile("^(?:!|&|\\*)$");
protected Map<Character, List<ResolverTuple>> yamlImplicitResolvers = new HashMap<Character, List<ResolverTuple>>();
protected void addImplicitResolvers() {
addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO");
/*
* INT must be before FLOAT because the regular expression for FLOAT
* matches INT (see issue 130)
* http://code.google.com/p/snakeyaml/issues/detail?id=130
*/
addImplicitResolver(Tag.INT, INT, "-+0123456789");
addImplicitResolver(Tag.FLOAT, FLOAT, "-+0123456789.");
addImplicitResolver(Tag.MERGE, MERGE, "<");
addImplicitResolver(Tag.NULL, NULL, "~nN\0");
addImplicitResolver(Tag.NULL, EMPTY, null);
addImplicitResolver(Tag.TIMESTAMP, TIMESTAMP, "0123456789");
// The following implicit resolver is only for documentation
// purposes.
// It cannot work
// because plain scalars cannot start with '!', '&', or '*'.
addImplicitResolver(Tag.YAML, YAML, "!&*");
}
public Resolver() {
addImplicitResolvers();
}
public void addImplicitResolver(Tag tag, Pattern regexp, String first) {
if (first == null) {
List<ResolverTuple> curr = yamlImplicitResolvers.get(null);
if (curr == null) {
curr = new ArrayList<ResolverTuple>();
yamlImplicitResolvers.put(null, curr);
}
curr.add(new ResolverTuple(tag, regexp));
} else {
char[] chrs = first.toCharArray();
for (int i = 0, j = chrs.length; i < j; i++) {
Character theC = Character.valueOf(chrs[i]);
if (theC == 0) {
// special case: for null
theC = null;
}
List<ResolverTuple> curr = yamlImplicitResolvers.get(theC);
if (curr == null) {
curr = new ArrayList<ResolverTuple>();
yamlImplicitResolvers.put(theC, curr);
}
curr.add(new ResolverTuple(tag, regexp));
}
}
}
//判斷配置項數據類型的關鍵方法
//value就是配置項的值,例如"09:00:00","10:00:00",到這里value還都是正常的
//這段代碼很簡單
public Tag resolve(NodeId kind, String value, boolean implicit) {
if (kind == NodeId.scalar && implicit) {
final List<ResolverTuple> resolvers;
if (value.length() == 0) {
resolqivers = yamlImplicitResolvers.get('\0');
} else {
//根據字符串的第一個字符來拿到一個解析器ResolverTuple,這個類也很簡單,
//里面就是一個預設的Tag對象和一個正則表達式,也就是本類的那些靜態變量
resolvers = yamlImplicitResolvers.get(value.charAt(0));
}
//開始通過解析器解析,解析器的作用就是通過對應的正則表達式來匹配配置項的值,如果匹配上了,則表示該配置項為該數據類型
//無論式09:00:00還是10:00:00,第一個值都是數字,所以最終拿到的解析器的正則表達式就是本類的靜態變量的INT和FLOAT
//匹配之后,INT的正則表達式能匹配上10:00:00,所以10:00:00被判斷為INT類型,至於為啥INT類型被轉換成了秒數,下文會再提,
//09:00:00沒有匹配上這個INT的正則表達式,所以就接着往下走
//所以說到底,這個問題是由於正則表達式匹配引發的問題
if (resolvers != null) {
for (ResolverTuple v : resolvers) {
Tag tag = v.getTag();
Pattern regexp = v.getRegexp();
if (regexp.matcher(value).matches()) {
return tag;
}
}
}
if (yamlImplicitResolvers.containsKey(null)) {
for (ResolverTuple v : yamlImplicitResolvers.get(null)) {
Tag tag = v.getTag();
Pattern regexp = v.getRegexp();
if (regexp.matcher(value).matches()) {
return tag;
}
}
}
}
switch (kind) {
case scalar:
//09:00:00最終走了這里的默認值,被判斷為STR類型
return Tag.STR;
case sequence:
return Tag.SEQ;
default:
return Tag.MAP;
}
}
}
org.yaml.snakeyaml.resolver.ResolverTuple
/**
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.yaml.snakeyaml.resolver;
import java.util.regex.Pattern;
import org.yaml.snakeyaml.nodes.Tag;
final class ResolverTuple {
private final Tag tag;
private final Pattern regexp;
public ResolverTuple(Tag tag, Pattern regexp) {
this.tag = tag;
this.regexp = regexp;
}
public Tag getTag() {
return tag;
}
public Pattern getRegexp() {
return regexp;
}
@Override
public String toString() {
return "Tuple tag=" + tag + " regexp=" + regexp;
}
}
INT類型的10:00:00為什么會被解析為秒數(36000)
org.yaml.snakeyaml.constructor.BaseConstructor
/**
* Construct complete YAML document. Call the second step in case of
* recursive structures. At the end cleans all the state.
*
* @param node root Node
* @return Java instance
*/
protected final Object constructDocument(Node node) {
//往里走
Object data = constructObject(node);
fillRecursive();
constructedObjects.clear();
recursiveObjects.clear();
return data;
}
/**
* Construct object from the specified Node. Return existing instance if the
* node is already constructed.
*
* @param node Node to be constructed
* @return Java instance
*/
protected Object constructObject(Node node) {
if (constructedObjects.containsKey(node)) {
return constructedObjects.get(node);
}
//往里走
return constructObjectNoCheck(node);
}
protected Object constructObjectNoCheck(Node node) {
if (recursiveObjects.contains(node)) {
throw new ConstructorException(null, null, "found unconstructable recursive node",
node.getStartMark());
}
recursiveObjects.add(node);
//這里會根據node拿到對應的構造器org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlInt#construct
//因為前面判斷的tag為int類型,所以最終拿到的構造器就是int類型的構造器
Construct constructor = getConstructor(node);
Object data = (constructedObjects.containsKey(node)) ? constructedObjects.get(node)
//這里就是調用轉換器轉換的地方
: constructor.construct(node);
finalizeConstruction(node, data);
constructedObjects.put(node, data);
recursiveObjects.remove(node);
if (node.isTwoStepsConstruction()) {
constructor.construct2ndStep(node, data);
}
return data;
}
org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlInt(轉換器代碼)
public class ConstructYamlInt extends AbstractConstruct {
@Override
public Object construct(Node node) {
String value = constructScalar((ScalarNode) node).toString().replaceAll("_", "");
int sign = +1;
char first = value.charAt(0);
if (first == '-') {
sign = -1;
value = value.substring(1);
} else if (first == '+') {
value = value.substring(1);
}
int base = 10;
//進制判斷
if ("0".equals(value)) {
return Integer.valueOf(0);
//二進制
} else if (value.startsWith("0b")) {
value = value.substring(2);
base = 2;
//十六進制
} else if (value.startsWith("0x")) {
value = value.substring(2);
base = 16;
} else if (value.startsWith("0")) {
value = value.substring(1);
base = 8;
//因為含有冒號,本次10:00:00進入了這個case,,最終進行了時間秒數的轉換
} else if (value.indexOf(':') != -1) {
String[] digits = value.split(":");
int bes = 1;
int val = 0;
for (int i = 0, j = digits.length; i < j; i++) {
val += Long.parseLong(digits[j - i - 1]) * bes;
bes *= 60;
}
return createNumber(sign, String.valueOf(val), 10);
} else {
return createNumber(sign, value, 10);
}
return createNumber(sign, value, base);
}
}