两大传统艺能学习完毕了,其中Spring Security还需要深入一下,呃,其实是整个Spring都还需要再深入学习。

光有了前边的两大传统艺能,如果还是使用JSP来开发页面,还没有做到前后端分离,还是拿着传统艺能在做传统的事情。

REST具体是什么就不多说了,在Django时代已经用过一次了。现在来看看Spring Web开发里如何使用REST,并且要把原来的增删改查项目,升级到REST风格的项目。

REST的部分将学习如下内容:

  1. 使用Spring 创建 REST 服务
  2. REST的理念,JSON和HTTP messaging
  3. 使用Postman REST客户端工具
  4. 使用@RestController开发REST API/Web 服务
  5. 最后使用上述概念写一个CRUD应用

Spring的文档地址:https://docs.spring.io/spring/docs/current/spring-framework-reference/index.html

REST基础概念

REST指的是REpresentational State Transfer,由于都是老Web开发了,具体就不介绍了,可以看其他文章。

REST的数据可以使用XML或者JSON,现在JSON使用的比较普遍也更加现代。

其实REST数据主要是给其他程序用的,而页面是给人看的,记住这点就可以了。

天气网站经常使用REST服务,比如https://openweathermap.org/,可以以api.openweathermap.org/data/2.5/weather?q={city name}的方式访问这个网站,当然,直接访问不行,还得有API KEY,这只是举一个例子。

通常这个网站会返回一个JSON,里边有所查询的城市的一系列天气的信息,这些信息实际上是给其他程序提供的,如果我们编写一个程序,让用户输入城市名,然后通过JS调用这个API,得到JSON之后展示给用户,此时这个天气网站就变成了我们页面的后端服务。这就是真正的前后端分离,只交换数据,页面不会由后端进行渲染。

在Java中使用JSON

JSON的基础概念对于我们学过前端的,就不细细学习了JSON,一句话,相当于一个键值对的组合。

这里要学的是如何用Java处理Json,由于Java基于类,所以很显然,需要一个POJO对应一种JSON,以便将Json解析成类对象,或者将类对象转换成JSON字符串。我们称之Java POJO和JSON的绑定,用英文来说,一般称作binding,Mapping映射,Serialization/Deserialization序列化/反序列化,Marshalling/Unmarshalling。看到这些单词,基本都是指同样的意思。

Spring使用Jackson Project来处理Java POJO和JSON的绑定。这个项目的GIT地址在这里

Jackson Project的包名是com.fasterxml.jackson.databind,Maven配置如下:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.0</version>
</dependency>

来通过代码学习一下Jackson的使用,其实用过JSON的都知道,JSON处理库基本上就两个大功能,一个是把数据转换成JSON字符串,一个是把JSON字符串解析成对应的数据类型。

新建一个Maven quickstart项目,然后把Jackson的依赖添加进去。

从JSON中读取数据

在src目录下创建data目录,编写两个JSON文件如下:

不嵌套的sample-lite.json:

{
	"id": 14,
	"firstName": "Mario",
	"lastName": "Rossi",
	"active": true
}

嵌套的sample-full.json:

{
	"id": 14,
	"firstName": "Mario",
	"lastName": "Rossi",
	"active": true,
	"address": {
		"street": "100 Main St",
		"city": "Philadelphia",
		"state": "Pennsylvania",
		"zip": "19103",
		"country": "USA"
	},
	"languages" : ["Java", "C#", "Python", "Javascript"]
}

之后第一步是先来创建映射的POJO,先从不嵌套的JSON开始:

package cc.conyli.json;

public class Student {
    private int id;
    private String firstName;
    private String lastName;
    boolean active;


    public Student() {
    }

    public Student(int id, String firstName, String lastName, boolean active) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.active = active;
    }

    public int getId() {
        return id;
    }

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

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", active=" + active +
                '}';
    }
}

类本身很简单,这里要注意的是,类的域名称必须要和JSON字符串里的键名相匹配,在实际操作中,Jackson不像Spring那样装配类,所以不会直接去访问域,而是调用getter和setter方法来写入和读取数据,所以getter和setter方法也要设置好。

然后来使用Jackson从JSON中载入数据到Student对象:

package cc.conyli.json;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;

public class JsonTest {

    public static void main(String[] args) {

        try {
//            创建新的映射对象
            ObjectMapper mapper = new ObjectMapper();

            //尝试载入JSON文件和对应的POJO类
            Student student = mapper.readValue(new File("src/data/sample-lite.json"), Student.class);

            //使用Student对象
            System.out.println("From JSON load a student: ---> " + student);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

这个其实是样板代码,需要创建一个映射对象,通过从一个文件对象中和对应的POJO类,自动加载对应的数据。

再来看一下那个嵌套的JSON,实际上JSON字符串中复杂结构映射到Java的类上边只有两种,要么还是一个普通对象,要么是一个List<String>对象。所以其中的address对应一个Address对象,而languages对应一个List<String>对象。知道了这些,就可以编写对应的类了:

package cc.conyli.json;

public class Address {

    private String street;
    private String city;
    private String state;
    private String zip;
    private String country;

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String getZip() {
        return zip;
    }

    public void setZip(String zip) {
        this.zip = zip;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

    public Address() {
    }

    public Address(String street, String city, String state, String zip, String country) {
        this.street = street;
        this.city = city;
        this.state = state;
        this.zip = zip;
        this.country = country;
    }

    @Override
    public String toString() {
        return "Address{" +
                "street='" + street + '\'' +
                ", city='" + city + '\'' +
                ", state='" + state + '\'' +
                ", zip='" + zip + '\'' +
                ", country='" + country + '\'' +
                '}';
    }
}

这个是纯POJO,没什么好说的,剩下就是要新建一个StudentComplext类来对应整个JSON,设置两个域为Address类型和List<String>类型。

package cc.conyli.json;

import java.util.List;

public class StudentComplex {
    ......
    private Address address;
    private List<String> languages;

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public List<String> getLanguages() {
        return languages;
    }

    public void setLanguages(List<String> languages) {
        this.languages = languages;
    }

    ......
}

也是很简单的一个POJO,其他域、构造器和toString()都省略了。然后来加载这个新的JSON文件。

public class App
{
    public static void main(String[] args) {

        try {
            ObjectMapper mapper = new ObjectMapper();

            //加载复杂的类
            StudentComplex student = mapper.readValue(new File("src/data/sample-full.json"), StudentComplex.class);

            System.out.println("From lite JSON load a student: ---> " + student);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

这样就完成了将JSON数据加载到对应的POJO类的过程,可见其中的关键是创建合理的POJO类来对应JSON文件。

仅加载需要的JSON数据

在日常开发中经常会遇到,仅需要JSON的部分数据,为此每次都用完整的类读入全部数据,再进行分析有些浪费。

现在我们不再需要JSON中的lastName数据,如果直接尝试把Student类中的lastName字段和getter/setter方法去掉,会报错Unrecognized field "lastName"

正确的做法是给Student类加上一个注解如下:

@JsonIgnoreProperties(ignoreUnknown = true)
public class Student {
    private int id;
    private String firstName;
    boolean active;
    ...
}

此时再运行,就发现仅取出了所需要的数据。

将POJO转换成JSON字符串

还记得很久之前学过的知识,想要把一个东西进行序列化,需要实现一个标志性接口叫做Serializable。

现在我们修改一下Student类,加上这个接口(代码省略)。Jackson序列化的方式依然操作ObjectMapper对象,直接调用write方法,传入一个输出流和数据对象即可。

public static void main(String[] args) {
    try {

        ObjectMapper mapper = new ObjectMapper();

        //新建一个学生数据
        Student student = new Student(1, "cony", "li", true);

        //向指定的流中写入转换后的字符串
        mapper.writeValue(System.out, student);

    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

注意.writeValue()方法调用一次之后流就关闭了,无法再写入,写入Map对象的例子如下:

public class App {
    public static void main(String[] args) {
        try {
            ObjectMapper mapper = new ObjectMapper();

            //创建Map对象
            Map<String, String> address = new HashMap<String, String>();
            address.put("street", "100 Main St");
            address.put("city", "Philadelphia");
            address.put("state", "Pennsylvania");
            address.put("zip", "19103");
            address.put("country", "USA");

            //写入转换后的JSON字符串到输出流
            mapper.writeValue(System.out, address);

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

单独这么转换JSON字符串没有什么大用处,我们想要的是将字符流写入Http响应对象中,也就是在Spring中使用Jackson。

Spring对于Jackson的支持是集成式的,无需向上边一样手动操作,Spring会自动将传递给REST控制器的JSON字符串转换成对应的POJO,将REST控制器返回的对象转换成JSON字符串,所以先要了解Spring基础的@RestController,再来看如何将JSON字符串以HTTP响应的形式返回。