本文为《从零打造项目》系列第二篇文章,首发于个人网站。
《从零打造项目》系列文章
东西
- 比MyBatis Generator更强壮的代码生成器
ORM结构选型
- SpringBoot项目根底设施搭建
- SpringBoot集成Mybatis项目实操
- SpringBoot集成Mybatis Plus项目实操
- SpringBoot集成Spring Data JPA项目实操
数据库改变办理
- 数据库改变办理:Liquibase or Flyway
- SpringBoot结合Liquibase完成数据库改变办理
守时使命结构
- Java守时使命技术剖析
- SpringBoot结合Quartz完成守时使命
- SpringBoot结合XXL-JOB完成守时使命
缓存
- Spring Security结合Redis完成缓存功用
安全结构
- Java运用程序安全结构
- Spring Security系列文章
- Spring Security结合JWT完成认证与授权
开发规范
- 后端必知:遵从Google Java规范并引进checkstyle检查
前语
精确点说,这不是《从零打造项目》系列的第一篇文章,模版代码生成的那个项目讲解算是第一篇,当时就打算做一套项目脚手架,为后续进行项目练习做准备。因时刻及个人经历问题,一直拖到现在才持续施行该计划,希望这次能顺利完成。
每个项目中都会有一些共用的代码,咱们称之为项目的根底设施,随拿随用。本文主要介绍 SpringBoot 项目中的一些根底设施,后续还会详细介绍 SpringBoot 别离结合 Mybatis、MybatisPlus、JPA 这三种 ORM 结构进行项目搭建,加深咱们对项目的把握才能。
因内容篇幅过长,本来这些根底设施代码应该散布在未来的三篇文章中,被提取出来,专门写一篇文章来介绍。
SpringBoot项目根底代码
引进依靠
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
</parent>
<properties>
<java.version>1.8</java.version>
<fastjson.version>1.2.73</fastjson.version>
<hutool.version>5.5.1</hutool.version>
<mysql.version>8.0.19</mysql.version>
<mybatis.version>2.1.4</mybatis.version>
<mapper.version>4.1.5</mapper.version>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.20</org.projectlombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.4.6</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.9</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.18</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
有些依靠不一定是最新版别,并且你看到这篇文章时,或许现已发布了新版别,到时分能够先仿照着将项目跑起来后,再依据自己的需求来升级各项依靠,有问题咱再解决问题。
日志恳求切面
项目进入联调阶段,服务层的接口需求和协议层进行交互,协议层需求将入参[json字符串]拼装成服务层所需的 json 字符串,拼装的进程中很简略犯错。入参犯错导致接口调试失利问题在联调中呈现很屡次,因而就想写一个恳求日志切面把入参信息打印一下,同时协议层调用服务层接口称号对不上也呈现了几次,经过恳求日志切面就能够知道上层是否有没有建议调用,便利前后端甩锅还能拿出证据。
首先界说一个恳求日志类,记载一些要害信息。
@Data
@EqualsAndHashCode(callSuper = false)
public class RequestLog {
// 恳求ip
private String ip;
// 拜访url
private String url;
// 恳求类型
private String httpMethod;
// 恳求办法名(绝对途径)
private String classMethod;
// 恳求办法描绘
private String methodDesc;
// 恳求参数
private Object requestParams;
// 回来成果
private Object result;
// 操作时刻
private Long operateTime;
// 耗费时刻
private Long timeCost;
// 过错信息
private JSONObject errorMessage;
}
然后依据 @Aspect 完成日志切面记载
@Component
@Aspect
@Slf4j
public class RequestLogAspect {
@Pointcut("execution(* com.msdn.orm.hresh.controller..*(..))")
public void requestServer() {
}
@Around("requestServer()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long start = System.currentTimeMillis();
//获取当时恳求方针
RequestLog requestLog = getRequestLog();
Object result = proceedingJoinPoint.proceed();
Signature signature = proceedingJoinPoint.getSignature();
// 恳求办法名(绝对途径)
requestLog.setClassMethod(String.format("%s.%s", signature.getDeclaringTypeName(),
signature.getName()));
// 恳求参数
requestLog.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));
// 回来成果
requestLog.setResult(result);
// 假如回来成果不为null,则从回来成果中剔除回来数据,检查条目数、回来状况和回来信息等
if (!ObjectUtils.isEmpty(result)) {
JSONObject jsonObject = JSONUtil.parseObj(result);
Object data = jsonObject.get("data");
if (!ObjectUtils.isEmpty(data) && data.toString().length() > 200) {
// 减少日志记载量,比方大量查询成果,没必要记载
jsonObject.remove("data");
requestLog.setResult(jsonObject);
}
}
// 获取恳求办法的描绘注解信息
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(Operation.class)) {
Operation methodAnnotation = method.getAnnotation(Operation.class);
requestLog.setMethodDesc(methodAnnotation.description());
}
// 耗费时刻
requestLog.setTimeCost(System.currentTimeMillis() - start);
log.info("Request Info : {}", JSONUtil.toJsonStr(requestLog));
return result;
}
@AfterThrowing(pointcut = "requestServer()", throwing = "e")
public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {
try {
RequestLog requestLog = getRequestLog();
Signature signature = joinPoint.getSignature();
// 恳求办法名(绝对途径)
requestLog.setClassMethod(String.format("%s.%s", signature.getDeclaringTypeName(),
signature.getName()));
// 恳求参数
requestLog.setRequestParams(getRequestParamsByJoinPoint(joinPoint));
StackTraceElement[] stackTrace = e.getStackTrace();
// 将反常信息转化成json
JSONObject jsonObject = new JSONObject();
if (!ObjectUtils.isEmpty(stackTrace)) {
StackTraceElement stackTraceElement = stackTrace[0];
jsonObject = JSONUtil.parseObj(JSONUtil.toJsonStr(stackTraceElement));
// 转化成json
jsonObject.set("errorContent", e.getMessage());
jsonObject.set("createTime", DateUtil.date());
jsonObject.setDateFormat(DatePattern.NORM_DATETIME_PATTERN);
jsonObject.set("messageId", IdUtil.fastSimpleUUID());
// 获取IP地址
jsonObject.set("serverIp", NetUtil.getLocalhostStr());
}
requestLog.setErrorMessage(jsonObject);
log.error("Error Request Info : {}", JSONUtil.toJsonStr(requestLog));
} catch (Exception exception) {
log.error(exception.getMessage());
}
}
private RequestLog getRequestLog() {
//获取当时恳求方针
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
// 记载恳求信息(经过Logstash传入Elasticsearch)
RequestLog requestLog = new RequestLog();
if (!ObjectUtils.isEmpty(attributes) && !ObjectUtils.isEmpty(attributes.getRequest())) {
HttpServletRequest request = attributes.getRequest();
// 恳求ip
requestLog.setIp(request.getRemoteAddr());
// 拜访url
requestLog.setUrl(request.getRequestURL().toString());
// 恳求类型
requestLog.setHttpMethod(request.getMethod());
}
return requestLog;
}
/**
* 依据办法和传入的参数获取恳求参数
*
* @param proceedingJoinPoint 入参
* @return 回来
*/
private Map<String, Object> getRequestParamsByProceedingJoinPoint(
ProceedingJoinPoint proceedingJoinPoint) {
//参数名
String[] paramNames = ((MethodSignature) proceedingJoinPoint.getSignature())
.getParameterNames();
//参数值
Object[] paramValues = proceedingJoinPoint.getArgs();
return buildRequestParam(paramNames, paramValues);
}
private Map<String, Object> getRequestParamsByJoinPoint(JoinPoint joinPoint) {
try {
//参数名
String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
//参数值
Object[] paramValues = joinPoint.getArgs();
return buildRequestParam(paramNames, paramValues);
} catch (Exception e) {
return new HashMap<>();
}
}
private Map<String, Object> buildRequestParam(String[] paramNames, Object[] paramValues) {
try {
Map<String, Object> requestParams = new HashMap<>(paramNames.length);
for (int i = 0; i < paramNames.length; i++) {
Object value = paramValues[i];
//假如是文件方针
if (value instanceof MultipartFile) {
MultipartFile file = (MultipartFile) value;
//获取文件名
value = file.getOriginalFilename();
}
requestParams.put(paramNames[i], value);
}
return requestParams;
} catch (Exception e) {
return new HashMap<>(1);
}
}
}
上述切面是在履行 Controller 办法时,打印出调用方IP、恳求URL、HTTP 恳求类型、调用的办法名、耗时等。
除了上述这种办法进行日志记载,还能够自界说注解,
@Target({ElementType.PARAMETER, ElementType.METHOD})//作用于参数或办法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {
/**
* 日志描绘
* @return
*/
String description() default "";
}
详细运用为:
@GetMapping(value = "/queryPage")
@Operation(description = "获取用户分页列表")
@SystemLog(description = "获取用户分页列表")
public Result<PageResult<UserVO>> queryPage(
@RequestBody UserQueryPageDTO dto) {
Page<UserVO> userVOPage = userService.queryPage(dto);
return Result.ok(PageResult.ok(userVOPage));
}
咱们只需求修正一下 RequestLogAspect 文件中的 requestServer()办法
@Pointcut("@annotation(com.xxx.annotation.SystemLog)")
public void requestServer() {
}
除了便利前后端排查问题,健壮的项目还会做日志剖析,这儿介绍一种我了解的日志剖析系统——ELK(ELasticsearch+Logstash+Kibana),在 RequestLogAspect 文件中能够将日志信息输出到 ELK 上,本项目不做过多介绍。
除了日志剖析,还有一种玩法,假如项目比较杂乱,比方说散布式项目,微服务个数过多,一次恳求往往需求涉及到多个服务,这样一来,调用链路就会很杂乱,一旦呈现故障,怎么快速定位问题需求考虑。一种解决计划便是在日志记载时增加一个 traceId 字段,一条调用链路上的 traceId 是相同。
大局反常
在日常项目开发中,反常是常见的,虽然 SpringBoot 对于反常有自己的处理计划,可是对于开发人员不行友好。咱们想要友好地抛出反常,针对运转时反常,想要一套大局反常捕获手法。因而怎么处理好反常信息,对咱们后续开发至关重要。
关于大局反常处理,能够参阅这篇文章。
1、界说根底接口类
public interface IError {
/**
* 过错码
*/
String getResultCode();
/**
* 过错描绘
*/
String getResultMsg();
}
2、反常枚举类
public enum ExceptionEnum implements IError {
// 数据操作状况码和提示信息界说
SUCCESS("200", "操作成功"),
VALIDATE_FAILED("400", "参数检验失利"),
NOT_FOUND("404", "参数检验失利"),
UNAUTHORIZED("401", "暂未登录或token现已过期"),
FORBIDDEN("403", "没有相关权限"),
REQUEST_TIME_OUT("408", "恳求时刻超时"),
INTERNAL_SERVER_ERROR("500", "服务器内部过错!"),
SERVER_BUSY("503", "服务器正忙,请稍后再试!");
/**
* 过错码
*/
private String resultCode;
/**
* 过错描绘
*/
private String resultMsg;
private ExceptionEnum(String resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
}
@Override
public String getResultCode() {
return resultCode;
}
@Override
public String getResultMsg() {
return resultMsg;
}
}
3、自界说事务反常类
public class BusinessException extends RuntimeException {
/**
* 过错码
*/
private String errorCode;
/**
* 过错描绘
*/
private String errorMsg;
public BusinessException() {
super();
}
public BusinessException(IError error) {
super(error.getResultCode());
this.errorCode = error.getResultCode();
this.errorMsg = error.getResultMsg();
}
public BusinessException(IError error, Throwable cause) {
super(error.getResultCode(), cause);
this.errorCode = error.getResultCode();
this.errorMsg = error.getResultMsg();
}
public BusinessException(String message) {
super(message);
}
public BusinessException(String errorCode, String errorMsg) {
super(errorCode);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BusinessException(String errorCode, String errorMsg, Throwable cause) {
super(errorCode, cause);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BusinessException(Throwable cause) {
super(cause);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public static void validateFailed(String message) {
throw new BusinessException(ExceptionEnum.VALIDATE_FAILED.getResultCode(), message);
}
public static void fail(String message) {
throw new BusinessException(message);
}
public static void fail(IError error) {
throw new BusinessException(error);
}
public static void fail(String errorCode, String errorMsg) {
throw new BusinessException(errorCode, errorMsg);
}
}
4、大局反常处理类
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理自界说的api反常
*
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler(value = BusinessException.class)
public Result handle(BusinessException e) {
if (Objects.nonNull(e.getErrorCode())) {
log.error("发生事务反常!原因是:{}", e.getErrorMsg());
return Result.failed(e.getErrorCode(), e.getErrorMsg());
}
return Result.failed(e.getMessage());
}
/**
* 处理参数验证失利反常 依据json格局的数据传递,这种传递才会抛出MethodArgumentNotValidException反常
*
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String message = null;
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (Objects.nonNull(fieldError)) {
message = fieldError.getField() + fieldError.getDefaultMessage();
}
}
return Result.validateFailed(message);
}
/**
* 运用@Validated 来校验 JavaBean的参数,比方@NotNull、@NotBlank等等; post 恳求数据传递有两种办法,一种是依据form-data格局的数据传递,这种传递才会抛出BindException反常
*
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler(value = BindException.class)
public Result handleValidException(BindException e) {
BindingResult bindingResult = e.getBindingResult();
String message = null;
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
message = fieldError.getField() + fieldError.getDefaultMessage();
}
}
return Result.validateFailed(message);
}
}
一致回来格局
现在比较盛行的是依据 json 格局的数据交互。可是 json 仅仅音讯的格局,其间的内容还需求咱们自行设计。不管是 HTTP 接口还是 RPC 接口坚持回来值格局一致很重要,这将大大下降 client 的开发成本。
界说回来值四要素
-
boolean success ;是否成功。
-
T data ;成功时详细回来值,失利时为 null 。
-
String code ;成功时回来 200 ,失利时回来详细过错码。
-
String message ;成功时回来 null ,失利时回来详细过错音讯。
回来方针中会处理分页成果,普通的查询成果,反常等信息。
@Data
@NoArgsConstructor
public class Result<T> implements Serializable {
private T data;
private String code;
private String message;
private boolean success;
protected Result(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.success = true;
}
protected Result(String code, String message, T data, boolean success) {
this(code, message, data);
this.success = success;
}
public static <T> Result<T> ok() {
return ok((T) null);
}
/**
* 成功回来成果
*
* @param data 获取的数据
* @return
*/
public static <T> Result<T> ok(T data) {
return new Result<>(ExceptionEnum.SUCCESS.getResultCode(),
ExceptionEnum.SUCCESS.getResultMsg(), data);
}
/**
* 成功回来list成果
*
* @param list 获取的数据
* @return
*/
public static <T> Result<List<T>> ok(List<T> list) {
Result<List<T>> listResult = new Result<>(ExceptionEnum.SUCCESS.getResultCode(),
ExceptionEnum.SUCCESS.getResultMsg(), list);
return listResult;
}
/**
* 成功回来成果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> Result<T> ok(T data, String message) {
return new Result<>(ExceptionEnum.SUCCESS.getResultCode(), message, data);
}
/**
* 失利回来成果
*
* @param error 过错码
*/
public static <T> Result<T> failed(IError error) {
return new Result<>(error.getResultCode(), error.getResultMsg(), null, false);
}
/**
* 失利回来成果
*
* @param error 过错码
* @param message 过错信息
*/
public static <T> Result<T> failed(IError error, String message) {
return new Result<>(error.getResultCode(), message, null, false);
}
/**
* 失利回来成果
*
* @param errorCode 过错码
* @param message 过错信息
*/
public static <T> Result<T> failed(String errorCode, String message) {
return new Result<>(errorCode, message, null, false);
}
/**
* 失利回来成果
*
* @param message 提示信息
*/
public static <T> Result<T> failed(String message) {
return new Result<>(ExceptionEnum.INTERNAL_SERVER_ERROR.getResultCode(), message, null, false);
}
/**
* 失利回来成果
*/
public static <T> Result<T> failed() {
return failed(ExceptionEnum.INTERNAL_SERVER_ERROR);
}
/**
* 参数验证失利回来成果
*/
public static <T> Result<T> validateFailed() {
return failed(ExceptionEnum.VALIDATE_FAILED);
}
/**
* 参数验证失利回来成果
*
* @param message 提示信息
*/
public static <T> Result<T> validateFailed(String message) {
return new Result<>(ExceptionEnum.VALIDATE_FAILED.getResultCode(), message, null, false);
}
/**
* 未登录回来成果
*/
public static <T> Result<T> unauthorized(T data) {
return new Result<>(ExceptionEnum.UNAUTHORIZED.getResultCode(),
ExceptionEnum.UNAUTHORIZED.getResultMsg(), data, false);
}
/**
* 未授权回来成果
*/
public static <T> Result<T> forbidden(T data) {
return new Result<>(ExceptionEnum.FORBIDDEN.getResultCode(),
ExceptionEnum.FORBIDDEN.getResultMsg(), data, false);
}
@Override
public String toString() {
return toJSONString(this);
}
}
方针类型转化
在项目中,尤其是在服务层,经常要将服务中的 Dto 实体方针转化为 Entity 方针,以及将 Entity 方针转化为 VO 方针回来给前端展现。现在市面上有很多这样的东西包,比方 Spring 结构中就自带了 BeanUtils,使咱们进行这样的数据操作非常简略方便,但当数据量级特别大时,存在功能问题。因而咱们要选择一款优秀的东西——Mapstruct。
关于 Mapstruct 的介绍以及其他方针转化东西,能够参阅这两篇文章:Apache的BeanUtils、Spring的BeanUtils、Mapstruct、BeanCopier方针拷贝 和 MapStruct 才是王者
界说如下方针类型转化文件:
@Mapper(componentModel = "spring")
public interface UserStruct {
@Mapping(target = "jobVOS",source = "jobs")
UserVO modelToVO(User record);
@Mapping(target = "jobVOS",source = "jobs")
List<UserVO> modelToVO(List<User> records);
User voToModel(UserVO record);
List<User> voToModel(List<UserVO> records);
UserDTO modelToDTO(User record);
List<UserDTO> modelToDTO(List<User> records);
User dtoToModel(UserDTO record);
List<User> dtoToModel(List<UserDTO> records);
}
假如方针中的特点名不同,能够运用 @Mapping 注解进行声明,自动生成的 UserStructImpl.class 如下所示,这儿只展现部分代码。
@Component
public class UserStructImpl implements UserStruct {
@Override
public UserVO modelToVO(User record) {
if ( record == null ) {
return null;
}
UserVO userVO = new UserVO();
userVO.setJobVOS( jobListToJobVOList( record.getJobs() ) );
userVO.setName( record.getName() );
userVO.setAge( record.getAge() );
userVO.setAddress( record.getAddress() );
return userVO;
}
protected JobVO jobToJobVO(Job job) {
if ( job == null ) {
return null;
}
JobVO jobVO = new JobVO();
jobVO.setName( job.getName() );
jobVO.setAddress( job.getAddress() );
return jobVO;
}
protected List<JobVO> jobListToJobVOList(List<Job> list) {
if ( list == null ) {
return null;
}
List<JobVO> list1 = new ArrayList<JobVO>( list.size() );
for ( Job job : list ) {
list1.add( jobToJobVO( job ) );
}
return list1;
}
//.......
}
分组校验和自界说校验
@Validation是一套协助咱们持续对传输的参数进行数据校验的注解,经过装备 Validation 能够很轻松的完成对数据的约束。
@Validated作用在类、办法和参数上
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
在项目中咱们或许会遇到这样的场景:新增数据时某些字段需求进行判空校验,而修正数据时又需求校验另外一些字段,并且都是用同一个方针来封装这些字段,为了便于办理及代码的高雅,咱们决议引进分组校验。
创立分组,区分新增和修正以及其它情况下的参数校验。
public interface ValidateGroup {
/**
* 新增
*/
interface Add extends Default {
}
/**
* 删除
*/
interface Delete {
}
/**
* 修正
*/
interface Edit extends Default {
}
}
除了分组校验,validation 还答应咱们自界说校验器。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = EnumValidatorClass.class)
public @interface EnumValidator {
String[] value() default {};
boolean required() default true;
// 校验枚举值不存在时的报错信息
String message() default "enum is not found";
//将validator进行分类,不同的类group中会履行不同的validator操作
Class<?>[] groups() default {};
//主要是针对bean,很少运用
Class<? extends Payload>[] payload() default {};
}
其间 EnumValidatorClass
类主要是为了校验 EnumValidator
注解的,代码如下:
public class EnumValidatorClass implements ConstraintValidator<EnumValidator, Integer> {
private String[] values;
@Override
public void initialize(EnumValidator enumValidator) {
this.values = enumValidator.value();
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
boolean isValid = false;
if (value == null) {
//当状况为空时运用默许值
return true;
}
for (int i = 0; i < values.length; i++) {
if (values[i].equals(String.valueOf(value))) {
isValid = true;
break;
}
}
return isValid;
}
}
后续项目实践进程中会演示详细运用。
Liquibase
Liquibase 是一个用于跟踪、办理和运用数据库改变的开源的数据库重构东西。它将一切数据库的改变(包含结构和数据)都保存在 changelog 文件中,便于版别操控,它的方针是提供一种数据库类型无关的解决计划,经过履行 schema 类型的文件来到达搬迁。
方针:
Liquibase 施行端到端CI / CD要求将一切代码(包含数据库代码)检入版别操控系统,并作为软件发布进程的一部分进行部署。
关于 Liquibase 的学习这儿就不过多介绍了,推荐阅读这篇文章,咱们直接进入运用环节。
1、引进依靠
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>4.16.1</version>
</dependency>
2、application.yml 装备
spring:
liquibase:
enabled: true
change-log: classpath:liquibase/master.xml
# 记载版别日志表
database-change-log-table: databasechangelog
# 记载版别改变lock表
database-change-log-lock-table: databasechangeloglock
3、resource 目录下新建 master.xml 和 changelog 目录
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
<includeAll path="src/main/resources/liquibase/changelog"/>
</databaseChangeLog>
4、运转项目,数据库中会生成如下两张表:
- DATABASECHANGELOG 表
- DATABASECHANGELOGLOCK表
因为 yaml 文件中的装备,实践生成的表名为小写格局。
接下来该研讨怎么运用 liquibase 了,假如项目所衔接的数据库中现在没有一个表,那么你能够在网上找一下 changeset 的书写格局,然后仿照着来建表。假如数据库中有表,能够先履行 liquibase:generateChangeLog 指令,生成一份现有表的建表句子,文件输出途径既能够在 yaml 文件中增加,然后在 pom 文件中读取 yaml 文件;也能够直接在 pom 文件中增加。
#输出文件途径装备
outputChangeLogFile: src/main/resources/liquibase/out/out.xml
pom.xml
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<version>4.16.1</version>
<configuration>
<!--properties文件途径,该文件记载了数据库衔接信息等-->
<propertyFile>src/main/resources/application.yml</propertyFile>
<propertyFileWillOverride>true</propertyFileWillOverride>
<!--生成文件的途径-->
<!-- <outputChangeLogFile>src/main/resources/liquibase/out/out.xml</outputChangeLogFile>-
</configuration>
</plugin>
假如之后想要增加新表,则只需求在 liquibase/changelog 目录下新建好对应的 xml 文件,比方这个:
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet author="hresh" id="1664204549485-7">
<createTable remarks="用户" tableName="user">
<column name="id" type="VARCHAR(36)">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="VARCHAR(20)">
<constraints unique="true"/>
</column>
<column name="age" type="INT"/>
<column name="address" type="VARCHAR(100)"/>
<column name="created_date" type="timestamp"/>
<column name="last_modified_date" type="timestamp"/>
<column defaultValueBoolean="false" name="del_flag" type="BIT(1)">
<constraints nullable="false"/>
</column>
<column name="create_user_code" type="VARCHAR(36)"/>
<column name="create_user_name" type="VARCHAR(50)"/>
<column name="last_modified_code" type="VARCHAR(36)"/>
<column name="last_modified_name" type="VARCHAR(50)"/>
<column defaultValueNumeric="1" name="version" type="INT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
现在项目 resource 目录结构如下:
只需求运转该项目,就会处理 user.xml 中的 changeSet,并在数据库中生成 user 表,并且在 databasechangelog 中刺进一条记载,重复运转项目时,会判别 changeSetId 防止重复刺进。
为了更好的运用 liquibase,比方说经过指令行来生成一个 changelog 模版,最好能记载下创立时刻,然后咱们只需求修正里面的内容即可。
为了满足该需求,则需求自界说自界说 Maven 插件。
自界说Maven插件
创立一个 maven 项目 liquibase-changelog-generate,本项目具有生成 xml 和 yaml 两种格局的 changelog,个人觉得 yaml 格局的 changelog 可读性更高。
1、界说一个接口,提早准备好共用代码,主要是判别 changelog id 是否有不合法字符,并且生成 changelog name。
public interface LiquibaseChangeLog {
default String getChangeLogFileName(String sourceFolderPath) {
System.out.println("> Please enter the id of this change:");
Scanner scanner = new Scanner(System.in);
String changeId = scanner.nextLine();
if (StrUtil.isBlank(changeId)) {
return null;
}
String changeIdPattern = "^[a-z][a-z0-9_]*$";
Pattern pattern = Pattern.compile(changeIdPattern);
Matcher matcher = pattern.matcher(changeId);
if (!matcher.find()) {
System.out.println("Change id should match " + changeIdPattern);
return null;
}
if (isExistedChangeId(changeId, sourceFolderPath)) {
System.out.println("Duplicate change id :" + changeId);
return null;
}
Date now = new Date();
String timestamp = DateUtil.format(now, "yyyyMMdd_HHmmss_SSS");
return timestamp + "__" + changeId;
}
default boolean isExistedChangeId(String changeId, String sourceFolderPath) {
File file = new File(sourceFolderPath);
File[] files = file.listFiles();
if (null == files) {
return false;
}
for (File f : files) {
if (f.isFile()) {
if (f.getName().contains(changeId)) {
return true;
}
}
}
return false;
}
}
2、每个 changelog 文件中的 changeSet 都有一个 author 特点,用来标注是谁创立的 changelog,现在我的做法是履行终端指令来获取 git 的 userName,假如有更好的完成,望不吝赐教。
public class GitUtil {
public static String getGitUserName() {
try {
String cmd = "git config user.name";
Process p = Runtime.getRuntime().exec(cmd);
InputStream is = p.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line = reader.readLine();
p.waitFor();
is.close();
reader.close();
p.destroy();
return line;
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return "hresh";
}
}
3、生成 xml 格局的 changelog
@Mojo(name = "generateModelChangeXml", defaultPhase = LifecyclePhase.PACKAGE)
public class LiquibaseChangeLogXml extends AbstractMojo implements LiquibaseChangeLog {
// 装备的是本maven插件的装备,在pom运用configration标签进行装备 property便是姓名,
// 在装备里面的标签姓名。在调用该插件的时分会看到
@Parameter(property = "sourceFolderPath")
private String sourceFolderPath;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
System.out.println("Create a new empty model changelog in liquibase yaml file.");
String userName = GitUtil.getGitUserName();
String changeLogFileName = getChangeLogFileName(sourceFolderPath);
if (StrUtil.isNotBlank(changeLogFileName)) {
generateXmlChangeLog(changeLogFileName, userName);
}
}
private void generateXmlChangeLog(String changeLogFileName, String userName) {
String changeLogFileFullName = changeLogFileName + ".xml";
File file = new File(sourceFolderPath, changeLogFileFullName);
String content = "<?xml version=\"1.1\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
+ "<databaseChangeLog xmlns=\"http://www.liquibase.org/xml/ns/dbchangelog\"\n"
+ " xmlns:ext=\"http://www.liquibase.org/xml/ns/dbchangelog-ext\"\n"
+ " xmlns:pro=\"http://www.liquibase.org/xml/ns/pro\"\n"
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
+ " xsi:schemaLocation=\"http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd\">\n"
+ " <changeSet author=\" " + userName + "\" id=\"" + changeLogFileName + "\">\n"
+ " </changeSet>\n"
+ "</databaseChangeLog>";
try {
FileWriter fw = new FileWriter(file.getAbsoluteFile());
BufferedWriter bw = new BufferedWriter(fw);
bw.write(content);
bw.close();
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4、生成 yaml 格局的 changelog
@Mojo(name = "generateModelChangeYaml", defaultPhase = LifecyclePhase.PACKAGE)
public class LiquibaseChangeLogYaml extends AbstractMojo implements LiquibaseChangeLog {
// 装备的是本maven插件的装备,在pom运用configration标签进行装备 property便是姓名,
// 在装备里面的标签姓名。在调用该插件的时分会看到
@Parameter(property = "sourceFolderPath")
private String sourceFolderPath;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
System.out.println("Create a new empty model changelog in liquibase yaml file.");
String userName = GitUtil.getGitUserName();
String changeLogFileName = getChangeLogFileName(sourceFolderPath);
if (StrUtil.isNotBlank(changeLogFileName)) {
generateYamlChangeLog(changeLogFileName, userName);
}
}
private void generateYamlChangeLog(String changeLogFileName, String userName) {
String changeLogFileFullName = changeLogFileName + ".yml";
File file = new File(sourceFolderPath, changeLogFileFullName);
String content = "databaseChangeLog:\n"
+ " - changeSet:\n"
+ " id: " + changeLogFileName + "\n"
+ " author: " + userName + "\n"
+ " changes:";
try {
FileWriter fw = new FileWriter(file.getAbsoluteFile());
BufferedWriter bw = new BufferedWriter(fw);
bw.write(content);
bw.close();
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5、履行 mvn install 指令,然后会在 maven 的 repository 文件中生成对应的 jar 包。
6、在 mybatis-springboot 引进 liquibase-changelog-generate
<plugin>
<groupId>com.msdn.hresh</groupId>
<artifactId>liquibase-changelog-generate</artifactId>
<version>1.0-SNAPSHOT</version>
<configuration>
<sourceFolderPath>src/main/resources/liquibase/changelog/
</sourceFolderPath><!-- 当时运用根目录 -->
</configuration>
</plugin>
7、点击如下任意一个指令
然后在操控台输入称号:job_create_table,效果为:
内容如下:
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet author="hresh" id="20220927_212841_214__job_create_table">
</changeSet>
</databaseChangeLog>
plugin-生成数据库修正文档
双击liquibase plugin面板中的liquibase:dbDoc
选项,会生成数据库修正文档,默许会生成到target
目录中,如下图所示
拜访index.html
会展现如下页面,几乎应有尽有
关于 liquibase 的更多有意思的运用,能够花时刻再去发掘一下,这儿就不过多介绍了。
一键式生成模版代码
依据 orm-generate 项目能够完成项目模板代码,集成了三种 ORM 办法:Mybatis、Mybatis-Plus 和 Spring JPA,JPA 是刚集成进来的,该项目上一年就现已发布过一版,也成功完成了想要的功用,关于功用介绍能够参阅我之前的这篇文章。
运转 orm-generate 项目,在 swagger 上调用 /build 接口,调用参数如下:
{
"database": "mysql_db",
"flat": true,
"type": "mybatis",
"group": "hresh",
"host": "127.0.0.1",
"module": "orm",
"password": "root",
"port": 3306,
"table": [
"user",
"job"
],
"username": "root",
"tableStartIndex":"0"
}
先将代码下载下来,解压出来目录如下:
代码文件直接移到项目中就行了,稍微修正一下引用就好了。
总结
上述根底代码是依据个人经历总结出来的,或许不行完美,乃至还短少一些更有价值的根底代码,望咱们多多指教。
在实践项目开发中,SpringBoot 根底代码和模版生成代码完全能够作为两个独立的项目,供其他事务项目运用,以上代码仅供参阅,运用时能够按需修正。