格式化JSON响应
带有 HTTP 状态 + JSON 内容响应结构体:
package com.app.fox.utils;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@Getter
public class JsonResponse {
/**
* 预定义空结构
*/
public static Map<String, Object> EMPTY = new HashMap<>(0);
private final String message;
private final Object data;
private final Integer time;
private JsonResponse(String message, Object data) {
this.message = message;
this.data = data == null ? EMPTY : data;
this.time = Math.toIntExact(System.currentTimeMillis() / 1000L);
}
public static ResponseEntity<JsonResponse> response(HttpStatus status, String message, Object data) {
return new ResponseEntity<>(new JsonResponse(message, data), status);
}
public static ResponseEntity<JsonResponse> success(String message, Object data) {
return response(HttpStatus.OK, message, data);
}
public static ResponseEntity<JsonResponse> success(Object data) {
return success(HttpStatus.OK.getReasonPhrase(), data);
}
public static ResponseEntity<JsonResponse> fail(String message) {
return response(HttpStatus.BAD_REQUEST, message, null);
}
}
RestController配置
常用 API 接口通用映射方式 RestControllerAdvice :
package com.app.fox.config;
import com.app.fox.exception.UnAuthorizedException;
import com.app.fox.utils.JsonResponse;
import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.util.HashMap;
import java.util.Objects;
@RestControllerAdvice
public class RestControllerAdviceConfig {
/**
* 参数验证器异常 spring-boot-starter-validation
*
* @param exception 转化异常
* @return JSON
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConversionFailedException.class)
public Object conversionException(RuntimeException exception) {
return JsonResponse.fail(exception.getMessage());
}
/**
* 同上
*
* @param exception 参数异常
* @return JSON
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Object argumentException(MethodArgumentNotValidException exception) {
String message = Objects.requireNonNull(exception.getBindingResult().getFieldError()).getDefaultMessage();
return JsonResponse.fail(message);
}
/**
* 提交类型异常
*
* @param exception 不支持类型
* @return JSON
*/
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
@ExceptionHandler(value = HttpMediaTypeNotSupportedException.class)
public Object argumentException(HttpMediaTypeNotSupportedException exception) {
return JsonResponse.response(HttpStatus.UNSUPPORTED_MEDIA_TYPE, exception.getMessage(), null);
}
/**
* 404异常
*
* @param exception 未找到资源
* @return JSON
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(value = NoResourceFoundException.class)
public Object noResourceFoundException(NoResourceFoundException exception) {
return JsonResponse.response(HttpStatus.NOT_FOUND, exception.getMessage(), null);
}
/**
* 自定义的未授权异常
*
* @param exception 自定义异常
* @return JSON
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(value = UnAuthorizedException.class)
public Object unAuthorizedException(UnAuthorizedException exception) {
return JsonResponse.response(HttpStatus.UNAUTHORIZED, exception.getMessage(), null);
}
}
参数结构体验证
需要追加依赖:
<dependencies>
<!-- SpringBoot参数验证组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 这里是需要内置的验证组件 @Valid -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
</dependency>
</dependencies>
之后可以直接声明表单对象结构:
package com.app.fox.forms;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
@Schema(name = "登录信息")
public class LoginInfoForm {
@NotBlank(message = "账户不能为空")
@Pattern(regexp = "^[a-zA-Z0-9]+$", message = "账号名称只允许字母数字")
@Size(message = "账户必须在{min}-{max}字符当中", min = 5, max = 64)
private String username;
@NotBlank(message = "密码不能为空")
@Size(message = "密码必须在{min}-{max}字符当中", min = 5, max = 64)
private String password;
}
权限拦截验证
需要先声明 HandlerInterceptor 拦截器:
package com.app.fox.config;
import com.app.fox.exception.UnAuthorizedException;
import com.app.fox.repository.models.UserInfoModel;
import com.app.fox.service.UserInfoService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class LoginInterceptorConfig implements HandlerInterceptor {
/**
* 日志句柄
*/
private final Logger logger = LoggerFactory.getLogger(getClass());
private final UserInfoService userInfoService;
public LoginInterceptorConfig(UserInfoService userInfoService) {
this.userInfoService = userInfoService;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
UserInfoModel userInfoModel = userInfoService.getOnlineUser(request);
logger.debug("启动鉴权:{}", userInfoModel);
return true;
}
@Override
public void postHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) throws Exception {
}
}
之后就是配置挂载服务 WebMvcConfigurer :
package com.app.fox.config;
import com.app.fox.service.UserInfoService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
@Order(1)
@Configuration
public class WebMvcConfigurerConfig implements WebMvcConfigurer {
private final UserInfoService userInfoService;
public WebMvcConfigurerConfig(UserInfoService userInfoService) {
this.userInfoService = userInfoService;
}
@Bean
public LoginInterceptorConfig interceptorConfig() {
return new LoginInterceptorConfig(userInfoService);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptorConfig())
.addPathPatterns(new ArrayList<>() {{
// 监控链接
add("/admin/**");
add("/business/**");
}})
.excludePathPatterns(new ArrayList<>() {{
// 排除拦截
add("/admin/user/login");
add("/admin/user/logout");
add("/admin/public/**");
add("/business/user/login");
add("/business/user/logout");
add("/business/public/**");
}});
}
}
启用Swagger
用于接口开发调试, 注意正式服要关闭 swagger 服务, 首先追加配置类文件:
package com.app.fox.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.awt.*;
/**
* Swagger配置
*/
@Configuration
public class SpringDocConfig {
/**
* 加载接口相关信息
* @return OpenAPI
*/
@Bean
public OpenAPI openAPI() {
Contact contact = new Contact();
contact.setName("MeteorCat");
contact.email("[email protected]");
contact.url("https://www.meteorcat.net");
Info info = new Info();
info.title("FoxApi调试");
info.description("FoxApi开发测试接口功能");
info.version("V1.0");
info.contact(contact);
// 授权有些模块采用 bearer 做请求验权
Components components = new Components();
SecurityScheme securityScheme = new SecurityScheme();
securityScheme.type(SecurityScheme.Type.HTTP);
securityScheme.scheme("bearer");
components.addSecuritySchemes("bearer-key", securityScheme);
return new OpenAPI()
.info(info)
.components(components);
}
/**
* 追加个 admin 路径的接口调试中心
* @return GroupedOpenApi
*/
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi
.builder()
.group("admin")
.pathsToMatch("/admin/**")
.build();
}
/**
* 追加个 business 路径的接口调试中心
* @return GroupedOpenApi
*/
@Bean
public GroupedOpenApi businessApi() {
return GroupedOpenApi
.builder()
.group("business")
.pathsToMatch("/business/**")
.build();
}
}
这里设置个复杂的文档配置:
package com.app.fox.controllers.admin;
import com.app.fox.forms.BusinessInfoForm;
import com.app.fox.service.UserInfoService;
import com.app.fox.utils.JsonResponse;
import com.app.fox.utils.ServerUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Tag(name = "admin.business", description = "后台商户用户")
@RequestMapping("/admin/business")
public class AdminBusiness {
/**
* 日志句柄
*/
final Logger logger = LoggerFactory.getLogger(getClass());
/**
* 用户数据
*/
final UserInfoService userInfoService;
final HttpServletRequest httpServletRequest;
public AdminBusiness(UserInfoService userInfoService, HttpServletRequest httpServletRequest) {
this.userInfoService = userInfoService;
this.httpServletRequest = httpServletRequest;
}
/**
* 创建商业用户
* 采用 Swagger , security 声明 Header带有 Authorized 授权才能访问
*
* @param businessInfoForm 账号结构体
* @return JSON
*/
@Operation(summary = "创建商业用户", description = "后台管理员创建商业用户", security = {@SecurityRequirement(name = "bearer-key")})
@PostMapping(path = "/create", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "创建成功", content = {
@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = JsonResponse.class))
}),
@ApiResponse(responseCode = "400", description = "创建失败", content = {
@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = JsonResponse.class))
})
})
public Object create(@Valid BusinessInfoForm businessInfoForm) {
String username = businessInfoForm.getUsername();
String nickname = businessInfoForm.getNickname();
String ipAddress = ServerUtils.getIpAddress(httpServletRequest);
nickname = nickname == null ? username : nickname;
logger.debug("create: {}", businessInfoForm);
// 检索出管理员
if (userInfoService.hasAdminUser(username)) {
return JsonResponse.fail("账号已存在");
}
// 检索出常规玩家
if (userInfoService.hasBusinessUser(username)) {
return JsonResponse.fail("账号已存在");
}
// 生成商户账号, 具体可以转发给
Long uid = userInfoService.getOnlineUserId(httpServletRequest);
userInfoService.createUser(
username,
businessInfoForm.getPassword(),
1,
nickname,
uid,
ipAddress
);
return JsonResponse.success(JsonResponse.EMPTY);
}
}
最后在 application-development.properties 启用配置:
# swagger 配置
springdoc.swagger-ui.enabled=true
springdoc.swagger-ui.path=/swagger/api.html
之后访问地址 http://127.0.0.1:8080/swagger/api.html
MySQL/MariaDB的JSON转换
新版本 MySQL/MariaDB 集成了新的 json 类型用来保存序列 Json 数据, 如果使用 JPA 可以通过编写转化器处理解析过程:
package com.app.fox.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
import org.springframework.web.server.ServerErrorException;
import java.io.IOException;
/**
* Json序列化转化器
* @param <T> 未知序列化类型
*/
public class ConvertObjectAndJson<T> implements AttributeConverter<T, String> {
private static final ObjectMapper mapper = new ObjectMapper();
/**
* 将 Object 对象转化为 Json String
* @param t 传入对象
* @return String
*/
@Override
public String convertToDatabaseColumn(T t) {
try {
return mapper.writeValueAsString(t);
} catch (JsonProcessingException e) {
throw new ServerErrorException(e.getMessage(), e.getCause());
}
}
/**
* 将 Json String 尝试序列化成实体对象
* @param s Json String
* @return 实体对象
*/
@Override
public T convertToEntityAttribute(String s) {
if (s == null || s.isBlank()) {
return null;
}
try {
return mapper.readValue(s, new TypeReference<>() {
});
} catch (IOException e) {
throw new ServerErrorException(e.getMessage(), e.getCause());
}
}
}
之后就是在 Jpa 当中声明 json 对象:
package com.app.fox.repository.models;
import com.app.fox.utils.ConvertObjectAndJson;
import jakarta.persistence.*;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
import java.util.Map;
@Data
@ToString
@Entity
@Table(name = "tbl_ad_browse")
public class AdBrowseModel implements Serializable {
@Id
@Column(nullable = false, length = 32, unique = true)
private String id;
@Column(nullable = false, length = 32, unique = true)
private String adId;
@Column(nullable = false)
private Integer createTime;
@Column(nullable = false, length = 64)
private String createIpAddress;
/**
* 尝试写入 JSON 对象并映射数据库, 这里就是关键点
* 需要 Convert 注解
*/
@Convert(converter = ConvertObjectAndJson.class)
@Column(nullable = false, columnDefinition = "JSON")
private Map<String, Object> data;
}
后续主要其他转化器都是可以 @Convert + AttributeConverter 做转化, 比如将数据库的 tinyint(1) - 1|0
转化位 true|false.
JPA 衍生 Convert 转化 Date
数据库常规用秒级时间戳保存int, 但是日常 Java 内部都是 date 形式, 这里可以自定义转化来处理:
package com.app.fox.utils;
import jakarta.persistence.AttributeConverter;
import java.util.Date;
/**
* 日期转化器
*/
public class ConvertDateAndInteger implements AttributeConverter<Date, Integer> {
/**
* 将 Date 转为时间戳
*
* @param date 日期对象
* @return Integer
*/
@Override
public Integer convertToDatabaseColumn(Date date) {
return Math.toIntExact(date.getTime() / 1000L);
}
/**
* 将时间戳转为date
*
* @param integer 秒级时间戳
* @return Date
*/
@Override
public Date convertToEntityAttribute(Integer integer) {
return new Date(integer * 1000L);
}
}
之后在 JPA 实体对象追加注解即可:
@Data
@ToString
@Entity
@Table(name = "tbl_user_info")
public class UserInfoModel implements Serializable {
/**
* 自定义时间转化器
*/
@Convert(converter = ConvertDateAndInteger.class)
@Column(nullable = false, columnDefinition = "INT")
private Date createTime;
/**
* 自定义时间转化器
*/
@Convert(converter = ConvertDateAndInteger.class)
@Column(nullable = false, columnDefinition = "INT")
private Date updateTime = new Date(0L);
}
这也就能自定义关联在 Java 的 Date 对象处理.
JPA动态分页
JPA内部已经集成分页功能, 只需要在 Repository 接口中实现 JpaSpecificationExecutor<T>:
/**
* 应用数据仓库工厂, 这里 JpaSpecificationExecutor 就是实现了分页功能
*/
public interface AppInfoRepository extends CrudRepository<AppInfoModel, Long>, JpaSpecificationExecutor<AppInfoModel> {
}
之后就是构建查询请求:
@RestController
@RequestMapping("/app")
public class App {
@GetMapping(path = "/list", consumes = MediaType.ALL_VALUE)
public Object list(@RequestParam Map<String, String> data) {
// 检索条件, 采用GET获取请求参数
String name = data.get("name");
String id = data.get("id");
// 复杂条件查询
Specification<AppInfoModel> specification = (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
// 确定条件存在附加上条件
if (name != null) {
predicates.add(criteriaBuilder.equal(root.get("name"), name));
}
if (id != null) {
predicates.add(criteriaBuilder.equal(root.get("id"), id));
}
Predicate[] arrays = new Predicate[predicates.size()];
return criteriaBuilder.and(predicates.toArray(arrays));
};
// 分页生成, 并且排序处理
int page = Integer.parseInt(data.getOrDefault("offset", "0"));
int total = Integer.parseInt(data.getOrDefault("total", "20"));
Pageable pageable = PageRequest.of(page, total, Sort.Direction.DESC,"id");
// 数据仓库查询, 这就能查询到分页数据
Page<AppInfoModel> result = repository.findAll(specification,pageable);
return result;
}
}