SpringMVC数据绑定

SpringMVC数据绑定

基本类型

对于基本类型是用int型还是Integer型?

如果传入int型参数:

1. 只能是int类型的,如果传入age="abc",如访问http://localhost/root?age=abc,会报400参数异常
2. int型:key是必传的,如果不传入,如访问http://localhost/root,会报500内部错误异常。

如果传入Integer类型,这是一个对int的包装类,本质上是一个对象:

可以不传key,那么访问http://localhost/root,则age属性为null即`age: null`

对于可能为空的数据,最好使用包装类型。还有一点,int型的默认值为0,Integer的默认值为null,如果数据库某字段允许NULL值,使用int型的话,存入的将是0而不是NULL。

举个例子,有这么一个方法。

@PostMapping("/getAge")
public String getAge(int age) {
    return "age: " + age;
}

当我们不传age字段时,因为参数是基本数据类型,当然会报500内部错误;当表单传输的字段名为age时,会自动绑定到Controller方法中的同名参数。如果把参数名改一改,

@PostMapping("/getAge")
public String getAge(int a) {
    return "age: " + a;
}

当表单传入age=66,此时在方法中找不到同名参数,自动绑定就会失效。如果像上面一样用的是int型,则会报500错误。并给出很贴心的错误提示如下

Optional int parameter 'age' is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type.

大概意思是说不能将age转换成null值,因为它被声明为一个原始类型了。并提示我们可以使用其相应的包装类。好,按照提示改成Integer,再运行上面程序。

不报错了,响应如下内容,说明数据绑定失效,虽然传入了age=66,但找不到同名参数,不能绑定到方法参数a上,所以参数a只好绑定null。而且,即使我们不传入age字段,也不会报错,响应内容依然和下面一样。(这就是推荐使用Integer而不使用int的原因之一)

age: null

可以使用@RequstParam将请求参数绑定到参数上,value属性明确指定表单字段的名称。

@PostMapping("/getAge")
public String getAge(@RequestParam(value = "age") Integer a) {
    return "age: " + a;
}

用value来明确指定表单传入的字段名,这个注解的意思是,将名为age字段值绑定到参数a上,使用@RequestParam不要求方法参数名和字段名一样,使得数据绑定时候更加灵活。注意当只有一个value属性时,可省略value =。该注解还有几个比较重要的属性

  • name:是value的别名,用哪个都可以
  • required:该字段是否必传,默认是true,如果不传会报错;改为false说明在前台不必传该子弹。
  • defaultValue:字段的默认值

来试一下,

@PostMapping("/getAge")
public String getAge(@RequestParam(value = "age", required = false, defaultValue = "5") Integer a) {
    return "age: " + a;
}

当访问http://localhost:8080/getAge,由于不必传age字段,而且指定了age的默认值为5,所以响应是

age: 5

数组

当传入的多个字段key都是同一个名称如下面的name,则会绑定到方法中的数组参数中。

Snipaste_2018-10-06_17-40-00
@PostMapping("/array")
public String array(@RequestParam("name") String[] names) {
    StringBuilder sb = new StringBuilder();
    for (String name : names) {
        sb.append(name).append(" ");
    }
    return sb.toString();
}

响应结果如下

abc def ghi 

当然如果是GET方式请求,访问http://localhost/array?name=abc&name=def&name=ghi 也是可以的。

SpringMVC可以将同一个字段key的多个值映射到一个数组中,那自然也能映射到List中。将参数String[] names改成List<String> names,完全没问题。

对象以及多层级对象

SpringMVC可以将表单的字段与Controller方法参数中的对象属性进行映射,前提是两者的名称和类型一致。

比如表单中传入name=Jim&age=33,方法参数中的User对象也恰好有String name与Integer age属性,SpringMVC将完成字段 --> 对象属性的映射并将数据绑定到User对象的对应属性上。

这里User类,有name和age字段,还有一个对象ContactInfo。

package com.shy.springmvcbingding.domain;

public class User {
    private String name;
    private Integer age;
    private ContactInfo contactInfo;
    // getter和setter方法省略
}


package com.shy.springmvcbingding.domain;

public class ContactInfo {
    private String address;
    private String phone;
    // getter和setter方法省略
}

在Controller中,方法参数可以是对象

// Controller被@RestController修饰
@PostMapping("/user")
public User getUser(User user) {
    return user;
}

如果我们传入以下参数

Snipaste_2018-10-06_20-46-17

响应输出为

{
    "name": "Jim",
    "age": 22,
    "contactInfo": {
        "address": "China",
        "phone": "13345678910"
    }
}

SpringMVC将我们输入字段的类型、名称与方法参数中User对象中的属性一一对比,如果吻合,将会自动映射过去并封装成对象,如果某些子弹和对象的属性不能匹配,封装成的对象中该属性为null。比如上面不小心将contactInfo.address写成了contactInfo.addr,响应是下面这样的,可以看到contactInfo的address字段为null了。

{
    "name": "Jim",
    "age": 33,
    "contactInfo": {
        "address": null,
        "phone": "13345678910"
    }
}

对于多层级的对象,如User中的ContactInfo类,该类又有address和phone两个属性。对于这种多层级的对象,需要使用.取到具体的属性。比如user.contactInfo.phone表示user实体中cantactInfo中的phone字段。因此在请求getUser方法时,若要将字段值注入到user中contactInfo的phone属性,需要传入contactInfo.phone

拥有同属性的多个对象

假如现在有一个Admin类,拥有和User一样的属性

package com.shy.springmvcbingding.domain;

/**
 * @author Haiyu
 * @date 2018/10/6 21:39
 */
public class Admin {
    private String name;
    private Integer age;
    private ContactInfo contactInfo;
    // getter setter方法省略
}

同时新增一个方法

@PostMapping("/userAndAdmin")
public String getUser(User user, Admin admin) {
    return user.toString() + " "+ admin.toString();
}

方法参数中既有User又有Admin,且两者拥有一样的属性。SpringMVC是如何进行绑定的呢?

User{name='Bob', age=22, contactInfo=ContactInfo{address='China', phone='13345678910'}} Admin{name='Bob', age=22, contactInfo=ContactInfo{address='China', phone='13345678910'}}

可以看到,它不做区分的给User和Admin进行了数据绑定。假如我们想有区分的进行绑定,要怎么做呢?可以使用@InitBinder注解

@InitBinder用于初始化WebDataBinder,在当前Controller类中有效。被@InitBinder注解的方法需要有WebDataBinder方法,且返回值一般是void,可以@RequstMapping方法调用之前,就对一些属性进行初始化。

如下是使用@InitBinder的例子

@InitBinder("user")
public void initUser(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("user.");
}

@InitBinder("admin")
public void initAdmin(WebDataBinder binder) {
    binder.setFieldDefaultPrefix("admin.");
}

它有一个属性value,不指定的话,默认作用于当前Controller的所有表单属性和请求参数(可以认为是@ModelAttribute和@RequstParam),如果指定了value值,就像上面那样,那么只针对参数是value值的attribute和param生效。比如@InitBinder("user")只对@ModelAttribute User user有效,@InitBinder("admin")只对@ModelAttribute Admin admin有效。综上,这两个方法的意义在于给参数user、admin分别设置参数前缀user.admin.这样不同的init-binder可以作用与不同的request param或model attribute

这下再来访问/userAndAdmin,可以看到我们给user.nameadmin.name赋予了不同的值

Snipaste_2018-10-06_22-11-34

有了前缀的区分,SpringMVC精准的将字段绑定到对应的类对象上了。

User{name='Bob', age=22, contactInfo=null} Admin{name='Lucy', age=21, contactInfo=null}

现在有个问题,如果我们不配置init-binder,参数也传上面那样的呢?

User{name='null', age=null, contactInfo=null} Admin{name='null', age=null, contactInfo=null}

由于没有给参数配置前缀,SpringMVC不认识user.name这样的属性了。

当然如果Admin的属性和User不一样,比如name改成adminName,age改成adminAge,那么是无需配置init-binder的。就像下面那样

Snipaste_2018-10-06_22-21-36

响应是

User{name='Lucy', age=21, contactInfo=null} Admin{adminName='Bob', adminAge=22, contactInfo=null}

SpringMVC会将匹配的字段分别绑定到了User和Admin对象上。

List、Set、Map

List

要将数据绑定到List<Integer>或者List<String>,其实是可以像下面这样写的。

public String array(@RequestParam("name") List<String> names) {...}
public String array(@RequestParam("age") List<Integer> ages) {...}

在表单中只要提交相同的字段即可name=bob&name=jim&name=lucy

但是数据要绑定到List<User>上,就不能这样写了因为User中还有其他属性。可以仿照上面User中的CantactInfo一样,用一个UserList类将List<User>包裹起来,对list某个元素的访问可以使用list[index]的形式。

package com.shy.springmvcbingding.domain;

import java.util.List;

public class UserList {
    private List<User> users;
    // getter和setter省略
}

相应的将Controller中的方法改成下面这样

@PostMapping("/list")
public UserList userList(UserList userList) {
    return userList;
}

因为userList中的List<User>其属性名是users,所以users[0]表示第一个user,users[1]表示第二个user,以此类推。

Snipaste_2018-10-07_09-36-56

可以看到,在表单中传入字段时,我们故意跳过了users[1],会报空指针异常吗?

{
    "users": [
        {
            "name": "vv",
            "age": 33,
            "contactInfo": null
        },
        {
            "name": null,
            "age": null,
            "contactInfo": null
        },
        {
            "name": "F",
            "age": 44,
            "contactInfo": null
        }
    ]
}

从响应中得知,被跳过的索引被设置成null了,并不会导致空指针。

Set

对于Set类型的数据绑定。先试着仿照List的做法。

package com.shy.springmvcbingding.domain;

import java.util.Set;

public class UserSet {
    Set<User> users;
    // getter和setter方法省略
}

Controller中新增方法set

@PostMapping("set")
public UserSet userSet(UserSet userSet) {
    return userSet;
}

表单中还是和List一样传入相同的字段。运行一下会发现报错了

Invalid property 'users[0]' of bean class [com.shy.springmvcbingding.domain.UserSet]: Cannot get element with index 0 from Set of size 0, accessed using property path 'users[0]'

追踪源码

else if (value instanceof Set) {
    // Apply index to Iterator in case of a Set.
    Set<Object> set = (Set<Object>) value;
    int index = Integer.parseInt(key);
    if (index < 0 || index >= set.size()) {
        throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
                                           "Cannot get element with index " + index + " from Set of size " +
                                           set.size() + ", accessed using property path '" + propertyName + "'");
    }

这里用到了set.size()是0,说明Set中没有任何对象。好吧,我那们就手动add几个进去。

给UserSet新增一个构造方法

public UserSet() {
    users = new LinkedHashSet<>();
    users.add(new User());
    users.add(new User());
    users.add(new User());
}

再运行程序就会得到和List一样结果。但是Set的主要功能是去重,当name和age一样时,就认为这两个对象是相等的,有个好的建议是对于自定义对象的Set和Map都必须重写hashCode方法equals方法

在User类中,重写这两个方法alt + insert让IDEA自动帮我们生成。

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return Objects.equals(name, user.name) &&
        Objects.equals(age, user.age);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

再运行程序,又报错了!原因还是越界了,因为在UserSet方法中连续add了三次User,但由于我们重写了hashCode和equals方法,这三个user是重复的。三次add后set的实际size是1。现在啊只传users[0].name=vv&users[0].age=33程序就不会报错了。

{
    "users": [
        {
            "name": "vv",
            "age": 33,
            "contactInfo": null
        }
    ]
}

Map

和上面一样先写UserMap

package com.shy.springmvcbingding.domain;

import java.util.Map;

public class UserMap {
    private Map<String,User> users;
    // getter和setter方法省略
}

在Controller中

@PostMapping("map")
public UserMap userMap(UserMap userMap) {
    return userMap;
}

然后用map['key']的形式传入以下参数即可。

Snipaste_2018-10-07_10-45-22

响应内容为

{
    "users": {
        "X": {
            "name": "Jack",
            "age": 22,
            "contactInfo": null
        },
        "Y": {
            "name": "Lucy",
            "age": 20,
            "contactInfo": null
        }
    }
}

Json&XML

使用注解@RequstBody可以处理contentType:"application/json"和“application/xml”编码的内容,并将json或者xml中的数据绑定到参数对象上。

先来测试Json,把contentType改成application/json,然后访问/json

@PostMapping("/json")
public User userJson(@RequestBody User user) {
    return user;
}

在request body中传入以下json内容

{
    "name":"bob",
    "age": 16
}

响应内容是

{
    "name": "bob",
    "age": 16,
    "contactInfo": null
}

同理,对于XML,contentType改成application/xml。在User类中,加几个xml绑定的注解。

package com.shy.springmvcbingding.domain;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.Objects;

@XmlRootElement(name = "user")
public class User {
    private String name;
    private Integer age;
    
    @XmlElement(name = "name")
    public String getName() {
        return name;
    }
    @XmlElement(name = "age")
    public Integer getAge() {
        return age;
    }

}

@XmlElement可以将Java属性映射成xml节点。

  • 在类上,加@XmlRootElement(name = "user"),表明xml的根元素名为user,如果不指定name,默认使用Java类名
  • 在getter/setter方法上加@XmlElement,将该属性映射成xml的对应节点。同样的,果不指定name,默认使用Java属性名

然后访问/xml,传入以下xml

<?xml version="1.0" encoding="UTF-8"?>
<user>
    <nam>Bob</nam>
    <age>22</age>
</user>

响应内容为

{
    "name": "Bob",
    "age": 22,
    "contactInfo": null
}

注意,@XmlElement最好用来getter方法上,如果用在了属性(Field)上,会引发下面的异常。

// @XmlElement(name = "name")
// private String name;

类的两个属性具有相同名称 "name"
    this problem is related to the following location:
        at public java.lang.String com.shy.springmvcbingding.domain.User.getName()
        at com.shy.springmvcbingding.domain.User
    this problem is related to the following location:
        at private java.lang.String com.shy.springmvcbingding.domain.User.name
        at com.shy.springmvcbingding.domain.User
] with root cause

日期转换

PropertyEditor

如果要绑定的类型是Date型的,在表单中传入date=2018-10-01,很明显会报错,说不能将String转换成Date型。

可以用init-binder为参数date注册一个PropertyEditor,其中有个子类CustomDateEditor

@InitBinder("date")
public void initDate(WebDataBinder binder) {
    binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
}

@PostMapping("date")
public String getDate(@RequestParam("date") Date date) {
    return date.toString();
}

其实就是将Date使用SimpleDateFormat来解析(parse)和格式化(format)。

来看下CustomDateEditor的源码,重点关注它重写的setAsText和getAsText方法

public CustomDateEditor(DateFormat dateFormat, boolean allowEmpty, int exactDateLength) {
    this.dateFormat = dateFormat;
    this.allowEmpty = allowEmpty;
    this.exactDateLength = exactDateLength;
}

@Override
public void setAsText(@Nullable String text) throws IllegalArgumentException {
    if (this.allowEmpty && !StringUtils.hasText(text)) {
        // Treat empty String as null value.
        setValue(null);
    }
    else if (text != null && this.exactDateLength >= 0 && text.length() != this.exactDateLength) {
        throw new IllegalArgumentException(
            "Could not parse date: it is not exactly" + this.exactDateLength + "characters long");
    }
    else {
        try {
            // 设置value,设置的是从String解析后的Date
            setValue(this.dateFormat.parse(text));
        }
        catch (ParseException ex) {
            throw new IllegalArgumentException("Could not parse date: " + ex.getMessage(), ex);
        }
    }
}

@Override
public String getAsText() {
    // 取出value并格式化
    Date value = (Date) getValue();
    return (value != null ? this.dateFormat.format(value) : "");
}

public void setValue(Object value) {
    this.value = value;
    firePropertyChange();
}

public Object getValue() {
    return value;
}

可以发现其实CustomDateEditor就是对DateFormat做了一个封装而已,参数allowEmpty表示是否允许text为空,该类会将空字符串当成null值。

现在运行程序,响应内容为

Mon Oct 01 00:00:00 CST 2018

成功将字符串2018-10-01转换成了Date型。

Formatter

package org.springframework.format下有这么一个接口

public interface Formatter<T> extends Printer<T>, Parser<T> {

}

Spring下有一个实现类DateFormatter,可以完成从字符串到日期的转换,直接使用即可。

不过需要在xml中引入FormattingConversionServiceFactoryBean并注入相应的property

<?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:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven conversion-service="dateFormatter"/>
    
    <bean id="dateFormatter" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="formatters">
            <set>
                <bean id="formatter" class="org.springframework.format.datetime.DateFormatter"/>
            </set>
        </property>
    </bean>
</beans>

定义好dateFormatter这个bean后,别忘了在mvc注解驱动中加上conversion-service,其属性值对应了FormattingConversionServiceFactoryBean的bean id。

<mvc:annotation-driven conversion-service="dateFormatter"/>

最后在Java Config中引入该xml文件

package com.shy.springmvcbingding.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

/**
 * @author Haiyu
 * @date 2018/10/7 22:33
 */
@Configuration
@ImportResource("classpath:bind.xml")
public class BindingConfig {
}

这样SpringBootApplication会扫描到这个配置文件并加载到上下文中。

Converter

FormattingConversionServiceFactoryBean这个类有两个重要的属性。

@Nullable
private Set<?> converters;

@Nullable
private Set<?> formatters;

可知它既可以实现Formatter也可以实现Converter。那么只需把注入的property属性改成converters即可。

<mvc:annotation-driven conversion-service="dateConverter"/>
    <bean id="dateConverter" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <!--<property name="formatters">-->
            <!--<set>-->
                <!--<bean id="formatter" class="org.springframework.format.datetime.DateFormatter"/>-->
            <!--</set>-->
        <!--</property>-->
        <property name="converters">
            <set>
                <bean id="converter" class="com.shy.springmvcbingding.converter.DateConverter">
                    <constructor-arg name="pattern" value="yyyy-MM-dd" />
                </bean>
            </set>
        </property>
    </bean>

DateConverter是自定义类,实现了Converter<S,T>接口,可以将S类型转换成T类型。如下,DateConverter<String, Date>是一个将String类型的转换成Date类型的转换器。

package com.shy.springmvcbingding.converter;

import org.springframework.core.convert.converter.Converter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author Haiyu
 * @date 2018/10/7 22:59
 */
public class DateConverter implements Converter<String, Date> {
    private String pattern;
    public DateConverter(String pattern) {
        super();
        this.pattern = pattern;
    }

    @Override
    public Date convert(String s) {
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        Date date = null;
        try {
            date =  sdf.parse(s);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}

@DateTimeFormat

将字符串转换成日期最方便的方法其实是在参数上添加注解@DateTimeFormat。

还可以为其指定pattern,如下。

@PostMapping("date")
public String getDate(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date) {
    return date.toString();
}

比较

  • PropertyEditor:局部使用,通常搭配WebDataBinder来使用
  • Formatter:全局/局部使用(xml中通过Spring注入的方式实现了全局使用),只能将String类型转换成其它类型
  • Converter:全局/局部使用(xml中通过Spring注入的方式实现了全局使用),可以将任意类型的转换成其他类型的。
  • @DateTimeFormat,局部使用,用在Controller的方法参数上

RESTful扩展

REST(Representational State Transfer)即表现层状态转移,如果一个架构符合REST原则,就称为RESTful架构。

资源(resource)包括文本、图片、音频、服务等,资源呈现的形式称为表现层。

  • 文本:txt、xml、json、html、binary
  • 图片:png、jpg

资源表现的形式通过http协议的content-type和accept来指定。

http://www.bookstore/book
这个网址只代表资源本身,资源的展现形式是通过content-type和accept来描述的

Accept代表发送端(客户端)希望接受的数据类型。
比如:Accept:text/xml; 代表客户端希望接受的数据类型是xml类型

Content-Type代表发送端(客户端|服务器)发送的实体数据的数据类型。Content-Type:text/html; 代表发送端发送的数据格式是html。

写一个方法查看请求的content-type。

// 方法1
@GetMapping("/contentType")
public String contentType(HttpServletRequest request) {
    return request.getContentType();
}

// 方法2
@GetMapping("/contentType")
public String contentType() {
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = servletRequestAttributes.getRequest();
    return request.getContentType();
}

在SpringMVC中,获取HttpServletRequest和HttpServletResponse有两种方法。

  • 在方法的入参中直接定义,如上方法1
  • 使用RequestContextHolder.getRequestAttributes(),然后调用其getRequest()和getResponse()就能获得HttpServletRequest和HttpServletResponse

HTTP请求方法中,最常用的是POST、DELETE、PUT、GET,可以认为分别对应了对资源的增删改查。再次之前西安介绍下HTTP中的幂等性:

幂等性:每次HTTP请求相同的参数,相同的URI,产生的结果是相同的。

  • GET-获取资源。如http://www.bookstore/book/123 表示查询编号为123的书籍
  • POST-创建资源,不具有幂等性。http://www.bookstore/book 表示新增一本书籍,请求多少次就会新增多少条记录,会响应不同的URI,即产生了不同的结果,所以认为POST不具有幂等性
  • PUT-创建(更新)资源,具有幂等性。http://www.bookstore/book/123 创建或者更新编号为123的书籍,如果编号123书籍已经存在那么语义是更新;如果不存在就是新增。但是多次请求的结果都是对一个资源的更新,即多次请求后结果不变,只会响应有一个URI,所以认为PUT是幂等的
  • DELETE-删除资源。http://www.bookstore/book/123 删除编号为123的书籍

以上4个请求方法中,POST不是幂等的,GET、DELETE、PUT是幂等的。

用RESTful风格写Controller,以上面的book为例

@GetMapping("/bookstore/book/{id}")
public String getBook(@PathVariable("id") Integer id) {
    return "Query book " + id;
}

@PostMapping("/bookstore/book")
public String addBook(@RequestParam("id") Integer id) {
    return "Add book " + id;
}

@PutMapping("/bookstore/book/{id}")
public String updateBook(@PathVariable("id") Integer id) {
    return "Update book " + id;
}
@DeleteMapping("/bookstore/book{id}")
public String deleteBook(@PathVariable("id") Integer id) {
    return "Delete book " + id;
}

注:@PathVariable可以将URL路径中{...}的内容绑定到参数上。

RESTful总结:

  • 每一个URI代表一个资源
  • 客户端和服务端之间,传递该资源的一种表现形式
  • 客户端通过不同的HTTP请求方法,对服务端的资源进行操作(增删改查等),实现表现层状态转移

by @sunhaiyu

2018.10.8

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,112评论 18 139
  • 这是我在 慕课网 观看 SpringMVC 数据绑定入门 所做的学习笔记其中包含对 **List,Set,Map,...
    jnil阅读 1,172评论 0 2
  • 既然找到这么一个写作的平台,我一不小心厚颜无耻地想到了自己先前的一本书。 这本书是几年前的一部悬疑作品,也是紫金 ...
    作家李禹东阅读 2,309评论 27 69
  • 文/一木 (一) 无法得赐一双写诗的手 只能把我母亲深深地埋怨 只能凭空想象 那些优秀诗人们手指修长而纯净 然而,...
    一木与你看世界阅读 309评论 0 0
  • 其实最开始写下的是不悲观,不绝望。后来一字一字删掉。 你是一个悲观主义者,这一点你从不否认,也不怀疑。 你赌下千年...
    宝剑与高跟鞋阅读 353评论 0 0