Springboot 解析yml配置文件中时间字符串异常 / Spring 解析YAML文件源码解析

示例代码和结果

先上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);
        }
    }

posted on 2020-09-17 16:10  precedeforetime  阅读(3195)  评论(0编辑  收藏  举报

导航