通用 JSON API

1 JSON 文档结构

  上篇介绍到 SpringBoot 整合 tk_Mybatis 中最后使用 JSONObject 返回 JSON,而在实际开发项目中,我们通常会统一 JSON 格式,便于前端后端人员进行交互。JSON 格式一般有成功和失败两种返回结果,接下来就介绍如何实现 JSON 格式。

1.1 SuccessResult 成功结果
{
    "links": {
        "self": "http://example.com/articles",
        "next": "http://example.com/articles?page[offset]=2",
        "last": "http://example.com/articles?page[offset]=10"
    },
    "data": [
        {
            "type": "articles",
            "id": "1",
            "attributes": {
                "title": "JSON API paints my bikeshed!"
            },
            "relationships": {},
            "links": {
                "self": "http://example.com/articles/1"
            }
        }
    ],
    "included": [],
    "meta": {
        "version": "1.0.0",
        "copyright": "Copyright 2015 Example Corp."
    }
}

data 属性

  一个典型的 data 的对象格式,我们的有效信息一般都放在 attributes 中。

{
    "type": "articles",
    "id": "1",
    "attributes": {
        "title": "JSON API paints my bikeshed!"
    },
    "relationships": {},
    "links": {
        "self": "http://example.com/articles/1"
    }
}
  • id: 显而易见为唯一标识,可以为数字也可以为hash字符串,取决于后端实现
  • type: 描述数据的类型,可以对应为数据模型的类名
  • attributes: 代表资源的具体数据
  • relationships、links: 为可选属性,用来放置关联数据和资源地址等数据

  具体 JSON 文档可参考 JSON API 中文版

links: 与 data 相关的链接对象。

1.2 errorResult 错误结果

  这里的 errors 和 data 有一点不同,一般来说返回值中 errors 作为列表存在,因为针对每个资源可能出现多个错误信息。最典型的例子为,我们请求的对象中某些字段不符合验证要求,这里需要返回验证信息,但是 HTTP 状态码会使用一个通用的 401,然后把具体的验证信息在 errors 给出来。

{
    "errors": [
        {
            "code": 10011,
            "title": "Name can't be null"
        },
        {
            "code": 10011,
            "title": "Content can't be null",
            "detail": ""
        }
    ]
}

  在 title 字段中给出错误信息,如果我们在本地或者开发环境想打出更多的调试堆栈信息,我们可以增加一个 detail 字段让调试更加方便。需要注意的一点是,我们应该在生产环境屏蔽部分敏感信息,detail 字段最好在生产环境不可见

2 实现 JSON API

  上面介绍了 JSON API,接下来开始实现具体细节,在 commons 包下创建 dto 包,并在 dto 包下创建如下类:

2.1 successResult

   @Data 的作用是不需要我们写 get、set方法,程序会在运行时自动生成。需要在 IDEA 中添加 Lombok 插件,如下图 2.1,并加入依赖,如下:
在这里插入图片描述
                 图 2.1

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<!-- 链式编程 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>guava</artifactId>
</dependency>

  成功结果代码

package com.pky.hello.springboot.commons.dto;

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

import java.util.List;

/**
 * 通用成功响应结果
 * @param <T>
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class SuccessResult<T extends AbstractBaseDomain> extends AbstractBaseResult {
    private Links links;
    private List<DataBean> data;

    /**
     * 构建删除响应结果
     * @param self
     */
    public SuccessResult(String self){
        links = new AbstractBaseResult.Links();
        links.setSelf(self);
    }

    /**
     * 构建单笔数据响应结果
     */
    public SuccessResult(String self, T attributes) {
        links = new Links();
        links.setSelf(self);

        data = Lists.newArrayList();
        createDataBean(null, attributes);
    }


    /**
     * 构建多笔数据响应结果
     * @param self
     * @param next
     * @param last
     */
    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 attributes) {
        if(data == null) {
            data = Lists.newArrayList();
        }

        DataBean dataBean = new DataBean();
//        dataBean.setId(attributes.getId());
        //设置类型,其中类型为实体类的类型
        dataBean.setType(attributes.getClass().getSimpleName());
        dataBean.setAttributes(attributes);

        //判断是否多条数据
        if(StringUtils.isNotBlank(self)) {
            Links links = new Links();
//            links.setSelf(self + "/" + attributes.getId());
            dataBean.setLinks(links);
        }

        data.add(dataBean);
    }
}
2.2 AbstractBaseDomain
package com.pky.hello.springboot.commons.dto;

import lombok.Data;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;

/**
 * 通用领域模型
 */
@Data
public class AbstractBaseDomain implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //用于在 json 中返回主键
    private Long id;
}

领域模型(实体类)如 TbUser 继承该类

2.3 AbstractBaseResult
package com.pky.hello.springboot.commons.dto;

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

import java.io.Serializable;

/**
 * 通用返回结果
 */
@Data
public class AbstractBaseResult implements Serializable {

    @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 extends AbstractBaseDomain> {
        private String type;
        private Long id;
        private T attributes;
        private T relationships;
        private Links links;
    }
}
2.4 BaseResultFactory
package com.pky.hello.springboot.commons.dto;

import com.alibaba.fastjson.JSONObject;

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

/**
 * 统一相应结果工厂
 * @param <T>
 */
public class BaseResultFactory<T extends AbstractBaseDomain> {

    private static final String LOGGER_LEVEL_DEBUG = "DEBUG";

    private static BaseResultFactory baseResultFactory;

    private BaseResultFactory() {

    }

    private static HttpServletResponse response;

    /**
     * 单例模式获取 BaseResultFactory 实例
     * @param response
     * @return
     */
    public static BaseResultFactory getInstance(HttpServletResponse response) {
        if(baseResultFactory == null) {
            synchronized (BaseResultFactory.class) {
                if(baseResultFactory == null) {
                    baseResultFactory = new BaseResultFactory();
                }
            }
        }
        BaseResultFactory.response = response;
        baseResultFactory.initResponse();
        return  baseResultFactory;
    }

    /**
     * 删除工厂
     * @param self
     * @return
     */
    public AbstractBaseResult build(String self){
        return new SuccessResult(self);
    }

    /**
     * 单笔数据工厂
     * @param self
     * @return
     */
    public AbstractBaseResult build(String self, T attributes) {
        return new SuccessResult(self, attributes);
    }

    /**
     * 多笔数据工厂
     * @param self
     * @param next
     * @param last
     * @return
     */
    public AbstractBaseResult build(String self, int next, int last, List<T> attributes) {
        return new SuccessResult(self, next, last, attributes);
    }

    /**
     * 错误信息工厂
     * @param code
     * @param title
     * @param detail(开发环境可见,生产环境屏蔽)
     * @return
     */
    public static AbstractBaseResult build(int code, String title, String detail, String level) {
        //设置请求失败的响应码
        response.setStatus(code);

        if(LOGGER_LEVEL_DEBUG.equals(level)) {
            return new ErrorResult(code, title, detail);
        } else {
            return new ErrorResult(code, title, null);
        }

    }

    /**
     * 设置响应头
     */
    private void initResponse(){
        response.setHeader("Content-Type", "application/vnd.api+json");
    }

}
2.5 ErrorResult
package com.pky.hello.springboot.commons.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * 通用错误返回结果
 */
@Data
@AllArgsConstructor   //创建有参的构造函数
public class ErrorResult extends AbstractBaseResult {

    private int code;
    private String title;
    private String detail;
}
2.6 AbstractBaseController

  在 controller 包下创建 base 包,并在 base 包下创建 AbstractBaseController 类。

package com.pky.hello.springboot.controller.base;

import com.pky.hello.springboot.commons.dto.AbstractBaseDomain;
import com.pky.hello.springboot.commons.dto.AbstractBaseResult;
import com.pky.hello.springboot.commons.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.CrossOrigin;
import org.springframework.web.bind.annotation.ModelAttribute;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

/**
 * 统一的 Controller
 * @param <T>
 */
@CrossOrigin //解决跨域
public class AbstractBaseController<T extends AbstractBaseDomain> {

    // 用于动态获取配置文件的属性值
    private static final String LOGGER_LEVEL_PETCLINIC = "logging.level.com.huanda.illegalquerypc";

    @Resource
    protected HttpServletRequest request;
    @Resource
    protected HttpServletResponse response;

    @Autowired
    private ConfigurableApplicationContext applicationContext;

    @ModelAttribute  //在所有的 @RequestMapping 前执行
    public void initReqAndRes(HttpServletRequest request, HttpServletResponse response){
        this.request = request;
        this.response = response;
    }

    /**
     * 删除成功响应结果1
     * @param self
     * @return
     */
    protected AbstractBaseResult success(String self){
        return BaseResultFactory.getInstance(response).build(self);
    }

    /**
     * 一条成功响应结果
     * @param self
     * @param attribute
     * @return
     */
    public AbstractBaseResult success(String self, T attribute){
        return BaseResultFactory.getInstance(response).build(self, attribute);
    }


    /**
     * 多条成功响应结果
     * @param self
     * @param next
     * @param last
     * @param attributes
     * @return
     */
    protected AbstractBaseResult success(String self, int next, int last, List<T> attributes){
        return BaseResultFactory.getInstance(response).build(self, next, last, attributes);
    }

    /**
     * 失败响应结果,为了降低响应状态码的重复率
     * @param title
     * @param detail
     * @return
     */
    protected AbstractBaseResult error(String title, String detail){
        return error(HttpStatus.UNAUTHORIZED.value(), title, detail);
    }

    /**
     * 失败响应结果
     * @param code
     * @param title
     * @param detail
     * @return
     */
    protected AbstractBaseResult error(int code, String title, String detail){
        return BaseResultFactory.getInstance(response).build(code, title, detail, applicationContext.getEnvironment().getProperty(LOGGER_LEVEL_PETCLINIC));
    }
}
2.7 LoginController
package com.pky.hello.springboot.controller;

import com.pky.hello.springboot.commons.domain.TbUser;
import com.pky.hello.springboot.commons.dto.AbstractBaseResult;
import com.pky.hello.springboot.commons.service.LoginService;
import com.pky.hello.springboot.commons.utils.BeanValidator;
import com.pky.hello.springboot.controller.base.AbstractBaseController;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "tb_users")
public class LoginController extends AbstractBaseController<TbUser> {

    @Autowired
    LoginService loginService;

    @GetMapping(value = "login")
    public AbstractBaseResult login(TbUser tbUser) {

        // 数据校验
        String message = BeanValidator.validator(tbUser);
        if(StringUtils.isNotBlank(message)) {
            return error(message, null);
        }
        // 登录校验
        TbUser user = loginService.getByLoginId(tbUser);
        // 登录成功
        if(user != null) {
            return success(request.getRequestURI(), user);
        }
        // 登录失败
        else {
            return error(401, "用户名或密码错误!", null);
        }
    }
}
2.8 LoginServiceImpl
package com.pky.hello.springboot.commons.service.impl;

import com.pky.hello.springboot.commons.domain.TbUser;
import com.pky.hello.springboot.commons.mapper.TbUserMapper;
import com.pky.hello.springboot.commons.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import tk.mybatis.mapper.entity.Example;

/**
 * 登录业务逻辑
 */
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    TbUserMapper tbUserMapper;

    /**
     * 通过用户名获取登录用户信息
     * @param tbUser
     * @return
     */
    @Override
    public TbUser getByLoginId(TbUser tbUser) {
        Example example = new Example(TbUser.class);
        // "username" 与实体类属性对应
        example.createCriteria().andEqualTo("username", tbUser.getUsername());
        TbUser user = tbUserMapper.selectOneByExample(example);

        boolean flag = false;
        // 查询成功
        if(user != null) {
            // 判断密码
            flag = checkPassword(user, tbUser.getPassword());
        }
        // 登录成功
        if(flag) {
            return user;
        }
        // 登录失败
        return null;
    }

    /**
     * 判断密码
     * @param tbUser
     * @param loginPwd 登录密码
     * @return
     */
    private Boolean checkPassword(TbUser tbUser, String loginPwd) {

        // 因数据库中的密码是 md5 加密,因此需要将登录密码进行加密后比较
        String password = DigestUtils.md5DigestAsHex(loginPwd.getBytes());

        // 密码正确
        if(tbUser.getPassword().equals(password)) {
            return true;
        }
        return false;
    }
}
2.9 测试

  浏览器输入 http://localhost:8082/v1/hello_springboot/tb_users/login?username=zhangsan&password=123456 ,结果如图 2.2、图 2.3 所示:

  • 成功结果
    在这里插入图片描述
                      图 2.2

  • 失败结果
    在这里插入图片描述
              图 2.3

SpringBoot 整合 tk_Mybatis <<

Logo

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

更多推荐