Spring自定义标签解析与实现

       在Spring Bean注册解析(一)Spring Bean注册解析(二)中我们讲到,Spring在解析xml文件中的标签的时候会区分当前的标签是四种基本标签(import、alias、bean和beans)还是自定义标签,如果是自定义标签,则会按照自定义标签的逻辑解析当前的标签。另外,即使是bean标签,其也可以使用自定义的属性或者使用自定义的子标签。本文将对自定义标签和自定义属性的使用方式进行讲解,并且会从源码的角度对自定义标签和自定义属性的实现方式进行讲解。

1. 自定义标签

1.1 使用方式

       对于自定义标签,其主要包含两个部分:命名空间和转换逻辑的定义,而对于自定义标签的使用,我们只需要按照自定义的命名空间规则,在Spring的xml文件中定义相关的bean即可。假设我们有一个类Apple,并且我们需要在xml文件使用自定义标签声明该Apple对象,如下是Apple的定义:

public class Apple {
  private int price;
  private String origin;

  public int getPrice() {
    return price;
  }

  public void setPrice(int price) {
    this.price = price;
  }

  public String getOrigin() {
    return origin;
  }

  public void setOrigin(String origin) {
    this.origin = origin;
  }
}

       如下是我们使用自定义标签在Spring的xml文件中为其声明对象的配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:myapple="http://www.lexueba.com/schema/apple"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.lexueba.com/schema/apple http://www.lexueba.com/schema/apple.xsd">

    <myapple:apple id="apple" price="123" origin="Asia"/>
</beans>

       我们这里使用了myapple:apple标签声明名为apple的bean,这里myapple就对应了上面的xmlns:myapple,其后指定了一个链接:http://www.lexueba.com/schema/apple,Spring在解析到该链接的时候,会到META-INF文件夹下找Spring.handlers和Spring.schemas文件(这里META-INF文件夹放在maven工程的resources目录下即可),然后读取这两个文件的内容,如下是其定义:

Spring.handlers
http\://www.lexueba.com/schema/apple=chapter4.eg3.MyNameSpaceHandler
Spring.schemas
http\://www.lexueba.com/schema/apple.xsd=META-INF/custom-apple.xsd

       可以看到,Spring.handlers指定了当前命名空间的处理逻辑类,而Spring.schemas则指定了一个xsd文件,该文件中则声明了myapple:apple各个属性的定义。我们首先看下自定义标签各属性的定义:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.lexueba.com/schema/apple"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://www.lexueba.com/schema/apple"
            elementFormDefault="qualified">

    <xsd:complexType name="apple">
        <xsd:attribute name="id" type="xsd:string">
            <xsd:annotation>
                <xsd:documentation>
                    <![CDATA[ The unique identifier for a bean. ]]>
                </xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
        <xsd:attribute name="price" type="xsd:int">
            <xsd:annotation>
                <xsd:documentation>
                    <![CDATA[ The price for a bean. ]]>
                </xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
        <xsd:attribute name="origin" type="xsd:string">
            <xsd:annotation>
                <xsd:documentation>
                    <![CDATA[ The origin of the bean. ]]>
                </xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
    </xsd:complexType>

    <xsd:element name="apple" type="apple">
        <xsd:annotation>
            <xsd:documentation><![CDATA[ The service config ]]></xsd:documentation>
        </xsd:annotation>
    </xsd:element>

</xsd:schema>

       可以看到,该xsd文件中声明了三个属性:id、price和origin。需要注意的是,这三个属性与我们的Apple对象的属性price和origin没有直接的关系,这里只是一个xsd文件的声明,以表征Spring的applicationContext.xml文件中使用当前命名空间时可以使用的标签属性。接下来我们看一下Spring.handlers中定义的MyNameSpaceHandler声明:

public class MyNameSpaceHandler extends NamespaceHandlerSupport {
  @Override
  public void init() {
    registerBeanDefinitionParser("apple", new AppleBeanDefinitionParser());
  }
}

       MyNameSpaceHandler只是注册了apple的标签的处理逻辑,真正的转换逻辑在AppleBeanDefinitionParser中。这里注册的apple必须与Spring的applicationContext.xml文件中myapple:apple标签后的apple保持一致,否则将找不到相应的处理逻辑。如下是AppleBeanDefinitionParser的处理逻辑:

public class AppleBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
  @Override
  protected Class<?> getBeanClass(Element element) {
    return Apple.class;
  }

  @Override
  protected void doParse(Element element, BeanDefinitionBuilder builder) {
    String price = element.getAttribute("price");
    String origin = element.getAttribute("origin");
    if (StringUtils.hasText(price)) {
      builder.addPropertyValue("price", Integer.parseInt(price));
    }

    if (StringUtils.hasText(origin)) {
      builder.addPropertyValue("origin", origin);
    }
  }
}

       可以看到,该处理逻辑中主要是获取当前标签中定义的price和origin属性的值,然后将其按照一定的处理逻辑注册到当前的BeanDefinition中。这里还实现了一个getBeanClass()方法,该方法用于表明当前自定义标签对应的BeanDefinition所代表的类的类型。如下是我们的入口程序,用于检查当前的自定义标签是否正常工作的:

public class CustomSchemaApp {
  public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    Apple apple = applicationContext.getBean(Apple.class);
    System.out.println(apple.getPrice() + ", " + apple.getOrigin());
  }
}

       运行结果如下:

123, Asia

1.2 实现方式

       我们还是从对整个applicationContext.xml文件开始读取的入口方法开始进行讲解,即DefaultBeanDefinitionDocumentReader.parseBeanDefinitions()方法,如下是该方法的源码:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    // 判断根节点使用的标签所对应的命名空间是否为Spring提供的默认命名空间,
    // 这里根节点为beans节点,该节点的命名空间通过其xmlns属性进行了定义
    if (delegate.isDefaultNamespace(root)) {
        NodeList nl = root.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            if (node instanceof Element) {
                Element ele = (Element) node;
                if (delegate.isDefaultNamespace(ele)) {
                    // 当前标签使用的是默认的命名空间,如bean标签,
                    // 则按照默认命名空间的逻辑对其进行处理
                    parseDefaultElement(ele, delegate);
                } else {
                    // 判断当前标签使用的命名空间是自定义的命名空间,如这里myapple:apple所
                    // 使用的就是自定义的命名空间,那么就按照定义命名空间逻辑进行处理
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        // 如果根节点使用的命名空间不是默认的命名空间,则按照自定义的命名空间进行处理
        delegate.parseCustomElement(root);
    }
}

       可以看到,该方法首先会判断当前文件指定的xmlns命名空间是否为默认命名空间,如果是,则按照默认命名空间进行处理,如果不是则直接按照自定义命名空间进行处理。这里需要注意的是,即使在默认的命名空间中,当前标签也可以使用自定义的命名空间,我们定义的myapple:apple就是这种类型,这里myapple就关联了xmlns:myapple后的myapple。如下是自定义命名空间的处理逻辑:

@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
    // 获取当前标签对应的命名空间指定的url
    String namespaceUri = getNamespaceURI(ele);
    if (namespaceUri == null) {
        return null;
    }
    
    // 获取当前url所对应的NameSpaceHandler处理逻辑,也即我们定义的MyNameSpaceHandler
    NamespaceHandler handler = this.readerContext
        .getNamespaceHandlerResolver()
        .resolve(namespaceUri);
    if (handler == null) {
        error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + 
              namespaceUri + "]", ele);
        return null;
    }
    
    // 调用当前命名空间处理逻辑的parse()方法,以对当前标签进行转换
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

       这里getNamespaceURI()方法的作用是获取当前标签对应的命名空间url。在获取url之后,会调用NamespaceHandlerResolver.resolve(String)方法,该方法会通过当前命名空间的url获取META-INF/Spring.handlers文件内容,并且查找当前命名空间url对应的处理逻辑类。如下是该方法的声明:

@Nullable
public NamespaceHandler resolve(String namespaceUri) {
    // 获取handlerMapping对象,其键为当前的命名空间url,
    // 值为当前命名空间的处理逻辑类对象,或者为处理逻辑类的包含全路径的类名
    Map<String, Object> handlerMappings = getHandlerMappings();
    // 查看是否存在当前url的处理类逻辑,没有则返回null
    Object handlerOrClassName = handlerMappings.get(namespaceUri);
    if (handlerOrClassName == null) {
        return null;
    } else if (handlerOrClassName instanceof NamespaceHandler) {
        // 如果存在当前url对应的处理类对象,则直接返回该处理对象
        return (NamespaceHandler) handlerOrClassName;
    } else {
        // 如果当前url对应的处理逻辑还是一个没初始化的全路径类名,则通过反射对其进行初始化
        String className = (String) handlerOrClassName;
        try {
            Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
            // 判断该全路径类是否为NamespaceHandler接口的实现类
            if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
                throw new FatalBeanException("Class [" + className + "] for namespace [" + 
                    namespaceUri + "] does not implement the [" + 
                    NamespaceHandler.class.getName() + "] interface");
            }
            NamespaceHandler namespaceHandler = 
                (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
            namespaceHandler.init();  // 调用处理逻辑的初始化方法
            handlerMappings.put(namespaceUri, namespaceHandler);  //缓存处理逻辑类对象
            return namespaceHandler;
        }
        catch (ClassNotFoundException ex) {
            throw new FatalBeanException("Could not find NamespaceHandler class [" 
               + className + "] for namespace [" + namespaceUri + "]", ex);
        }
        catch (LinkageError err) {
            throw new FatalBeanException("Unresolvable class definition for" 
               + "NamespaceHandler class [" + className + "] for namespace [" 
               +  namespaceUri + "]", err);
        }
    }
}

       可以看到,在处理命名空间url的时候,首先会判断是否存在当前url的处理逻辑,不存在则直接返回。如果存在,则会判断其为一个NamespaceHandler对象,还是一个全路径的类名,是NamespaceHandler对象则强制类型转换后返回,否则通过反射初始化该类,并调用其初始化方法,然后才返回。

       我们继续查看NamespaceHandler.parse()方法,如下是该方法的源码:

@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 获取当前标签使用的parser处理类
    BeanDefinitionParser parser = findParserForElement(element, parserContext);
    // 按照定义的parser处理类对当前标签进行处理,这里的处理类即我们定义的AppleBeanDefinitionParser
    return (parser != null ? parser.parse(element, parserContext) : null);
}

       这里的parse()方法首先会查找当前标签定义的处理逻辑对象,找到后则调用其parse()方法对其进行处理。这里的parser也即我们定义的AppleBeanDefinitionParser.parse()方法。这里需要注意的是,我们在前面讲过,在MyNameSpaceHandler.init()方法中注册的处理类逻辑的键(即apple)必须与xml文件中myapple:apple后的apple一致,这就是这里findParserForElement()方法查找BeanDefinitionParser处理逻辑的依据。如下是findParserForElement()方法的源码:

@Nullable
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    // 获取当前标签命名空间后的局部键名,即apple
    String localName = parserContext.getDelegate().getLocalName(element);
    // 通过使用的命名空间键获取对应的BeanDefinitionParser处理逻辑
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) {
        parserContext.getReaderContext().fatal(
           "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }
    return parser;
}

       这里首先获取当前标签的命名空间后的键名,即myapple:apple后的apple,然后在parsers中获取该键对应的BeanDefinitionParser对象。其实在MyNameSpaceHandler.init()方法中进行的注册工作就是将其注册到了parsers对象中。

2. 自定义属性

2.1 使用方式

       自定义属性的定义方式和自定义标签非常相似,其主要也是进行命名空间和转换逻辑的定义。假设我们有一个Car对象,我们需要使用自定义标签为其添加一个描述属性。如下是Car对象的定义:

public class Car {
  private long id;
  private String name;
  private String desc;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getDesc() {
    return desc;
  }

  public void setDesc(String desc) {
    this.desc = desc;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

       如下是在applicationContext.xml中该对象的定义:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:car="http://www.lexueba.com/schema/car-desc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="car" class="chapter4.eg2.Car" car:car-desc="This is test custom attribute">
        <property name="id" value="1"/>
        <property name="name" value="baoma"/>
    </bean>
</beans>

       可以看到,car对象的定义使用的就是一般的bean定义,只不过其多了一个属性car:car-desc的使用。这里的car:car-desc对应的命名空间就是上面的http://www.lexueba.com/schema/car-desc。同自定义标签一样,自定义属性也需要在META-INF下的Spring.handlers和Spring.schemas文件中指定当前的处理逻辑和xsd定义,如下是这两个文件的定义:

Spring.handlers
http\://www.lexueba.com/schema/car-desc=chapter4.eg2.MyCustomAttributeHandler
Spring.schemas
http\://www.lexueba.com/schema/car.xsd=META-INF/custom-attribute.xsd

       对应的xsd文件的定义如下:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.lexueba.com/schema/car-desc"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://www.lexueba.com/schema/car-desc"
            elementFormDefault="qualified">

    <xsd:attribute name="car-desc" type="xsd:string"/>

</xsd:schema>

       可以看到,该xsd文件中只定义了一个属性,即car-desc。如下是MyCustomAttributeHandler的声明:

public class MyCustomAttributeHandler extends NamespaceHandlerSupport {
  @Override
  public void init() {
    registerBeanDefinitionDecoratorForAttribute("car-desc", 
      new CarDescInitializingBeanDefinitionDecorator());
  }
}

       需要注意的是,和自定义标签不同的是,自定义标签是将处理逻辑注册到parsers对象中,这里自定义属性是将处理逻辑注册到attributeDecorators中。如下CarDescInitializingBeanDefinitionDecorator的逻辑:

public class CarDescInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {
  @Override
  public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
    String desc = ((Attr) node).getValue();
    definition.getBeanDefinition().getPropertyValues().addPropertyValue("desc", desc);
    return definition;
  }
}

       可以看到,对于car-desc的处理逻辑就是获取当前定义的属性的值,由于知道其是当前标签的一个属性,因而可以将其强转为一个Attr类型的对象,并获取其值,然后将其添加到指定的BeandDefinitionHolder中。这里需要注意的是,自定义标签继承的是AbstractSingleBeanDefinitionParser类,实际上是实现的BeanDefinitionParser接口,而自定义属性实现的则是BeanDefinitionDecorator接口。

2.2 实现方式

       关于自定义属性的实现方式,需要注意的是,自定义属性只能在bean标签中使用,因而我们可以直接进入对bean标签的处理逻辑中,即DefaultBeanDefinitionDocumentReader.processBeanDefinition()方法,如下是该方法的声明:

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
    // 对bean标签的默认属性和子标签进行处理,将其封装为一个BeanDefinition对象,
    // 并放入BeanDefinitionHolder中
    BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
    if (bdHolder != null) {
        // 进行自定义属性或自定义子标签的装饰
        bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
        try {
            // 注册当前的BeanDefinition
            BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder,
               getReaderContext().getRegistry());
        }catch (BeanDefinitionStoreException ex) {
            getReaderContext().error("Failed to register bean definition with name '" +
                                     bdHolder.getBeanName() + "'", ele, ex);
        }
        
        // 调用注册了bean标签解析完成的事件处理逻辑
        getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
    }
}

       这里我们直接进入BeanDefinitionParserDelegate.decorateBeanDefinitionIfRequired()方法中:

public BeanDefinitionHolder decorateBeanDefinitionIfRequired(
    Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) {

    BeanDefinitionHolder finalDefinition = definitionHolder;

    // 处理自定义属性
    NamedNodeMap attributes = ele.getAttributes();
    for (int i = 0; i < attributes.getLength(); i++) {
        Node node = attributes.item(i);
        finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
    }

    // 处理自定义子标签
    NodeList children = ele.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        Node node = children.item(i);
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
        }
    }
    return finalDefinition;
}

       可以看到,自定义属性和自定义子标签的解析都是通过decorateIfRequired()方法进行的,如下是该方法的定义:

public BeanDefinitionHolder decorateIfRequired(
    Node node, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) {

    // 获取当前自定义属性或子标签的命名空间url
    String namespaceUri = getNamespaceURI(node);
    // 判断其如果为spring默认的命名空间则不对其进行处理
    if (namespaceUri != null && !isDefaultNamespace(namespaceUri)) {
        // 获取当前命名空间对应的NamespaceHandler对象
        NamespaceHandler handler = this.readerContext
            .getNamespaceHandlerResolver()
            .resolve(namespaceUri);
        if (handler != null) {
            // 对当前的BeanDefinitionHolder进行装饰
            BeanDefinitionHolder decorated =
                handler.decorate(node, originalDef, 
                   new ParserContext(this.readerContext, this, containingBd));
            if (decorated != null) {
                return decorated;
            }
        }
        else if (namespaceUri.startsWith("http://www.springframework.org/")) {
            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + 
                  namespaceUri + "]", node);
        }
        else {
            // A custom namespace, not to be handled by Spring - maybe "xml:...".
            if (logger.isDebugEnabled()) {
                logger.debug("No Spring NamespaceHandler found for XML schema namespace [" 
                             + namespaceUri + "]");
            }
        }
    }
    return originalDef;
}

       decorateIfRequired()方法首先会获取当前自定义属性或子标签对应的命名空间url,然后根据该url获取当前命名空间对应的NamespaceHandler处理逻辑,并且调用其decorate()方法进行装饰,如下是该方法的实现:

@Nullable
public BeanDefinitionHolder decorate(
    Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
    // 获取当前自定义属性或子标签注册的BeanDefinitionDecorator对象
    BeanDefinitionDecorator decorator = findDecoratorForNode(node, parserContext);
    // 调用自定义的BeanDefinitionDecorator.decorate()方法进行装饰,
    // 这里就是我们实现的CarDescInitializingBeanDefinitionDecorator类
    return (decorator != null ? decorator.decorate(node, definition, parserContext) : null);
}

       和自定义标签不同的是,自定义属性或自定义子标签查找当前Decorator的方法是需要对属性或子标签进行分别判断的,如下是findDecoratorForNode()的实现:

@Nullable
private BeanDefinitionDecorator findDecoratorForNode(Node node,
        ParserContext parserContext) {
    BeanDefinitionDecorator decorator = null;
    // 获取当前标签或属性的局部键名
    String localName = parserContext.getDelegate().getLocalName(node);
    // 判断当前节点是属性还是子标签,根据情况不同获取不同的Decorator处理逻辑
    if (node instanceof Element) {
        decorator = this.decorators.get(localName);
    } else if (node instanceof Attr) {
        decorator = this.attributeDecorators.get(localName);
    } else {
        parserContext.getReaderContext().fatal(
            "Cannot decorate based on Nodes of type [" + node.getClass().getName() 
            + "]", node);
    }
    if (decorator == null) {
        parserContext.getReaderContext().fatal(
            "Cannot locate BeanDefinitionDecorator for " + (node instanceof Element 
            ? "element" : "attribute") + " [" + localName + "]", node);
    }
    return decorator;
}

       对于BeanDefinitionDecorator处理逻辑的查找,可以看到,其会根据节点的类型进行判断,根据不同的情况获取不同的BeanDefinitionDecorator处理对象。

3. 自定义子标签

       对于自定义子标签的使用,其与自定义标签的使用非常相似,不过需要注意的是,根据对自定义属性的源码解析,我们知道自定义子标签并不是自定义标签,自定义子标签只是起到对其父标签所定义的bean的一种装饰作用,因而自定义子标签的处理逻辑定义与自定义标签主要有两点不同:①在NamespaceHandler.init()方法中注册自定义子标签的处理逻辑时需要使用registerBeanDefinitionDecorator(String, BeanDefinitionDecorator)方法;②自定义子标签的处理逻辑需要实现的是BeanDefinitionDecorator接口。其余部分的使用都和自定义标签一致。

4. 总结

       本文主要对自定义标签,自定义属性和自定义子标签的使用方式和源码实现进行了讲解,有了对自定义标签的理解,我们可以在Spring的xml文件中根据自己的需要实现自己的处理逻辑。另外需要说明的是,Spring源码中也大量使用了自定义标签,比如spring的AOP的定义,其标签为<aspectj-autoproxy />。从另一个角度来看,我们前面两篇文章对Spring的xml文件的解析进行了讲解,可以知道,Spring默认只会处理import、alias、bean和beans四种标签,对于其余的标签,如我们所熟知的事务处理标签,这些都是使用自定义标签实现的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,679评论 6 342
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,223评论 1 92
  • 坚守 文||与你相识 三十六载的风雨 承载你的爱心 桃李满天下 岂能说尽所有的故事 你用最好的年华 滋养了大山的希...
    与你相识_40fa阅读 153评论 2 3
  • 主宰演讲台 作者:美国~比尔胡戈特伯 来源:行动营之未读书籍书籍。 感悟: 这本书是之前在行动营的时候买的,但是一...
    星星爱L欢阅读 119评论 0 1