JSON API

解决的问题

在微服务架构下,前后端分离的项目如何通过统一的数据结构进行数据交互。

服务架构

适用范围:微服务架构 + 前后端分离

参考文档

JSON API中文版
JSON API官方英文版

效果展示

返回单个对象

{
    "links": {
        "self": "/restful/records/1"
    },
    "data": [
        {
            "type": "User",
            "id": 1,
            "attributes": {
                "id": 1,
                "userId": 1,
                "username": "kino",
                "password": "aejics2329ijdsjhuas##44"
            }
        }
    ]
}

返回集合对象

{
    "links": {
        "self": "/restful/records",
        "next": "/restful/records?page=2",
        "last": "/restful/records?page=10"
    },
    "data": [
        {
            "type": "User",
            "id": 1,
            "attributes": {
                "id": 1,
                "userId": 1,
                "username": "kino",
                "password": "aejics2329ijdsjhuas##44"
            },
            "links": {
                "self": "/restful/records/1"
            }
        },
        {
            "type": "User",
            "id": 2,
            "attributes": {
                "id": 2,
                "userId": 2,
                "username": "kino iq",
                "password": "aejics2329ijdsjhuas##44"
            },
            "links": {
                "self": "/restful/records/2"
            }
        }
    ]
}

名词解析

详见 【参考文档 JSON API 中文版】

links 资源连接,比如连接到详情页
data 资源对象
data attributes 资源对象中的数据
relationshiops attributes的关联对象

实现步骤

相关技术栈

Spring Boot(2.2.5.RELEASE)
Spring Cloud(Hoxton.SR3)
Spring Cloud Alibaba(2.2.1.RELEASE)

因为该项目的职责是为了让前后端通过统一的RESTFUL风格的JSON API通信,所以不是按照完整的项目架构去搭建的,只在项目中集成了nacos,也没有提取统一的工具类库。

需求简述

按照统一的格式将数据通过JSON格式返回给前端
具体格式参照JSON API文档
能够返回单个对象
能够返回集合对象
对象属性为空时不返回null
请求错误时返回统一的错误信息
生成环境不显示错误信息中的detail信息
开发环境中显示错误的detail信息
detail错误信息的展示效果可以通过动态配置来实现

项目结构简图

在这里插入图片描述
在这里插入图片描述

统一的依赖管理

POM
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.kino</groupId>
    <artifactId>kino-dependencies</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>

    <properties>
        <java.version>13</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

RESTFUL JSON API 项目搭建

POM
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>kino-provider</artifactId>
    <packaging>jar</packaging>

    <parent>
        <groupId>com.kino</groupId>
        <artifactId>kino-dependencies</artifactId>
        <version>1.0.0</version>
        <relativePath>../kino-dependencies/pom.xml</relativePath>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>guava</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>kino.provider.KinoProviderApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
application
package kino.provider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class KinoProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(KinoProviderApplication.class,args);
    }
}
application.yml
spring:
  application:
    name: kino-provider
  cloud:
    nacos: 
      discovery:
        server-addr: 192.168.23.1:8848

server:
  port: 8083

management:
  endpoints:
    web:
      exposure:
        include: "*"

# Configure the log level of the package
# 作用解析:通过日志级别来实现error detail信息显示的动态配置
logging:
  level:
    kino:
      provider: INFO

核心代码

核心代码的关系图

(1)展示了如何相应请求
在这里插入图片描述
(2)如何按照要求构建相应数据(SUCCESS)
在这里插入图片描述

AbstractBaseDomain
package kino.provider.dto;

import lombok.Data;

import java.io.Serializable;

@Data
public abstract class AbstractBaseDomain implements Serializable {
    private Long id;
}
AbstractBaseResult
package kino.provider.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

@Data
public abstract class AbstractBaseResult {

    @Data
    @JsonInclude(JsonInclude.Include.NON_NULL)
    protected static class Links {
        private String self;
        private String next;
        private String last;
    }

    @Data
    @JsonInclude(JsonInclude.Include.NON_NULL)
    protected static class DataBean<T> {
        private String type;
        private Long id;
        private T attributes;
        private T relationships;
        private Links links;
    }
}
BaseResultFactory
package kino.provider.dto;

import java.util.List;

// 工厂模式
public class BaseResultFactory<T extends AbstractBaseDomain> {

    private static String LOGGER_LEVEL_DEBUG = "DEBUG";

    private static BaseResultFactory baseResultFactory;

    // 私有化构造函数,外部不能通过new获取对象
    private BaseResultFactory() {
    }

    public static BaseResultFactory getInstance() {
        // 双重锁机制(绝对单例)
        if (baseResultFactory == null) {
            synchronized (BaseResultFactory.class) {
                if (baseResultFactory == null) {
                    baseResultFactory = new BaseResultFactory();
                }
            }
        }
        return baseResultFactory;
    }

    public AbstractBaseResult build(String self, T attribute) {
        return new SuccessResult(self, attribute);
    }

    public AbstractBaseResult build(String self, int next, int last, List<T> attributes) {
        return new SuccessResult(self, next, last, attributes);
    }

    public AbstractBaseResult build(int code, String title, String detail, String level) {
        if (LOGGER_LEVEL_DEBUG.equals(level)) {
            return new ErrorResult(code, title, detail);
        } else {
            return new ErrorResult(code, title, null);
        }
    }
}
ErrorResult
package kino.provider.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;

/***
 * <p>
 *     逻辑说明:
 * 生成环境不显示detail详细
 * 只有在DEBUG模式下显示detail信息
 * </p>
 */
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@EqualsAndHashCode(callSuper = false) // 解决build时的警告
public class ErrorResult extends AbstractBaseResult {
    private int code;
    private String title;
    private String detail;
}
SuccessResult
package kino.provider.dto;

import com.google.common.collect.Lists;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;

import java.util.List;

@Data
@EqualsAndHashCode(callSuper = false) // 解决build时的警告
public class SuccessResult<T extends AbstractBaseDomain> extends AbstractBaseResult {
    private Links links;
    private List<DataBean> data;


    public SuccessResult(String self, T attribute) {
        links = new Links();
        links.setSelf(self);
        createDataBean(null, attribute);
    }

    public SuccessResult(String self, int next, int last, List<T> attributes) {
        links = new Links();
        links.setSelf(self);
        links.setNext(self + "?page=" + next);
        links.setLast(self + "?page=" + last);
        attributes.forEach(attribute -> createDataBean(self, attribute));
    }

    private void createDataBean(String self, T attribute) {
        if (data == null) {
            // guava 的链式编程
            data = Lists.newArrayList();
        }
        DataBean dataBean = new DataBean();
        dataBean.setId(attribute.getId()); // AbstractBaseDomain
        dataBean.setType(attribute.getClass().getSimpleName());
        dataBean.setAttributes(attribute);

        if (StringUtils.isNoneBlank(self)) {
            // 解决集合中每个对象都带links(page)
            Links links = new Links();
            links.setSelf(self + "/" + attribute.getId());
            dataBean.setLinks(links);
        }

        data.add(dataBean);
    }
}
RestfulApiController
package kino.provider.controller;

import com.google.common.collect.Lists;
import kino.provider.domain.User;
import kino.provider.dto.AbstractBaseResult;
import kino.provider.dto.BaseResultFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

@RestController
@RequestMapping(value = "restful")
public class RestfulApiController {

    // 动态刷新参数
    @Autowired
    private ConfigurableApplicationContext applicationContext;

    @GetMapping(value = "records/{id}")
    public AbstractBaseResult getById(HttpServletRequest request, @PathVariable long id) {
        // mock data
        User user = new User();
        user.setId(1L); //AbstractBaseDomain
        user.setUserId(1L);
        user.setUsername("kino");
        user.setPassword("aejics2329ijdsjhuas##44");
        if (id == 0) { // 检验detail数据不显示
            return BaseResultFactory.getInstance().build(HttpStatus.UNAUTHORIZED.value(), "参数类型错误", "ID 不能为0", applicationContext.getEnvironment().getProperty("logging.level.kino.provider"));
        } else {
            return BaseResultFactory.getInstance().build(request.getRequestURI(), user);
        }
    }

    @GetMapping(value = "records")
    public AbstractBaseResult getList(HttpServletRequest request) {
        // mock data
        User user = new User();
        user.setId(1L); //AbstractBaseDomain
        user.setUserId(1L);
        user.setUsername("kino");
        user.setPassword("aejics2329ijdsjhuas##44");
        User user2 = new User();
        user2.setId(2L); //AbstractBaseDomain
        user2.setUserId(2L);
        user2.setUsername("kino iq");
        user2.setPassword("aejics2329ijdsjhuas##44");
        List<User> list = Lists.newArrayList();
        list.add(user);
        list.add(user2);
        return BaseResultFactory.getInstance().build(request.getRequestURI(), 2, 10, list);
    }
}
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐