通用 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