Base工程是用来放公共组件的

依赖信息

引入如下依赖:

pom.xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>

实体类

learning-online-base工程下创建包com.swx.base.model,并在该包下创建两个实体类

分页查询参数

PageParam
/**
* 分页查询参数
*/
@Data
@ApiModel("分页查询参数")
public class PageParam {

// 当前页
@ApiModelProperty("当前页码")
private Long pageNo = 1L;
// 每页记录数
@ApiModelProperty("每页记录数")
private Long pageSize = 10L;

public PageParam() {
}

public PageParam(Long pageNo, Long pageSize) {
this.pageNo = pageNo;
this.pageSize = pageSize;
}
}

分页查询结果

PageResult
/**
* 分页查询结果模型类
*
* @param <T> 范型
*/
@Data
public class PageResult<T> implements Serializable {

// 数据列表
private List<T> items;
// 总记录数
private long counts;
// 当前页数
private long page;
// 每页记录数
private long pageSize;

public PageResult() {
}

public PageResult(List<T> items, long counts, long page, long pageSize) {
this.items = items;
this.counts = counts;
this.page = page;
this.pageSize = pageSize;
}

public PageResult(List<T> items, long counts, PageParam pageParam) {
this.items = items;
this.counts = counts;
this.page = pageParam.getPageNo();
this.pageSize = pageParam.getPageSize();
}
}

JSON格式化

com.swx.base.config创建配置类JSONConvertConfig

JSONConvertConfig
@Configuration
public class JSONConvertConfig {

/**
* 序列化内容
*/
@Bean
public LocalDateTimeSerializer localDateTimeSerializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

@Bean
public LocalDateTimeDeserializer localDateTimeDeserializer() {
return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
builder.serializerByType(LocalDateTime.class, localDateTimeSerializer());
builder.deserializerByType(LocalDateTime.class, localDateTimeDeserializer());
};
}

// Long 转 String
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
// 忽略value为null时key的输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(module);
return objectMapper;
}
}

Knife4j配置类

com.swx.base.config创建配置类Knife4jConfiguration

Knife4jConfiguration
@Configuration
@EnableSwagger2
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
public class Knife4jConfiguration {

@Bean(value = "defaultApi2")
public Docket defaultApi2() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("1.0")
.select()
.apis(RequestHandlerSelectors.basePackage("com.swx"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("学成在线API文档")
.description("学成在线API文档")
.version("1.0")
.build();
}
}

统一返回结果

为了让前端接受到统一的返回结果,需要使用拦截器自动包装Controller返回结果,同时全局拦截异常处理,并一起包装返回统一结果。

{
code: 200,
message: '成功',
data: null,
}

返回结果

learning-online-base工程的com.swx.base.model包下创建如下几个实体类

返回结果

R
/**
* 返回结果实体类
*/
@Data
public class R implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 返回码
*/
private Integer code;

/**
* 返回消息
*/
private String message;

/**
* 返回数据
*/
private Object data;

public R() {

}

public R(ResultCodeEnum resultCode, Object data) {
this.code = resultCode.code();
this.message = resultCode.message();
this.data = data;
}

private void setResultCode(ResultCodeEnum resultCode) {
this.code = resultCode.code();
this.message = resultCode.message();
}

// 返回成功
public static R success() {
R result = new R();
result.setResultCode(ResultCodeEnum.SUCCESS);
return result;
}
// 返回成功
public static R success(Object data) {
R result = new R();
result.setResultCode(ResultCodeEnum.SUCCESS);
result.setData(data);
return result;
}

// 返回失败
public static R fail(Integer code, String message) {
R result = new R();
result.setCode(code);
result.setMessage(message);
return result;
}
// 返回失败
public static R fail(ResultCodeEnum resultCode) {
R result = new R();
result.setResultCode(resultCode);
return result;
}
}


异常结果

ErrorResult
/**
* 异常结果包装类
* @author sw-code
*
*/
public class ErrorResult {

private Integer code;

private String message;

private String exception;

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public String getException() {
return exception;
}

public void setException(String exception) {
this.exception = exception;
}

public static ErrorResult fail(ResultCodeEnum resultCode, Throwable e, String message) {
ErrorResult errorResult = ErrorResult.fail(resultCode, e);
errorResult.setMessage(message);
return errorResult;
}

public static ErrorResult fail(ResultCodeEnum resultCode, Throwable e) {
ErrorResult errorResult = new ErrorResult();
errorResult.setCode(resultCode.code());
errorResult.setMessage(resultCode.message());
errorResult.setException(e.getClass().getName());
return errorResult;
}
public static ErrorResult fail(Integer code, String message) {
ErrorResult errorResult = new ErrorResult();
errorResult.setCode(code);
errorResult.setMessage(message);
return errorResult;
}
}


异常枚举类

ResultCodeEnum
public enum ResultCodeEnum {

/* 成功状态码 */
SUCCESS(200, "成功"),
/* Token 50~100 */
TOKEN_INVALID(50, "无效TOKEN"),
TOKEN_EXPIRE(51, "TOKEN已过期"),
TOKEN_REQUIRE(50, "TOKEN是必须的"),
/* 参数错误 500~1000 */
SERVER_ERROR(500, "服务器内部错误"),
PARAM_REQUIRE(501, "缺少参数"),
PARAM_INVALID(502, "无效参数"),
PARAM_TIMAGE_FORMAT_ERROR(503, "图片格式有误"),
/* 数据错误 1000~2000 */
DATA_EXIST(1000, "数据已经存在"),
AP_USER_DATA_NOT_EXIST(1001, "ApUser数据不存在"),
DATA_NOT_EXIST(1002, "数据不存在"),
/* 数据错误 300~3500 */
NO_OPERATOR_AUTH(3000, "无权操作"),
NEED_ADMIN(3001, "需要管理员权限");

private Integer code;
private String message;

private ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}

public Integer code() {
return this.code;
}
public String message() {
return this.message;
}
}

自定义异常

learning-online-base工程的com.swx.base.excpetion包下创建自定义异常

BizException
public class BizException extends RuntimeException {

/**
* 错误码
*/
private Integer code;

/**
* 错误信息
*/
private String message;

public BizException() {
super();
}

public BizException(ResultCodeEnum resultCode) {
super(resultCode.message());
this.code = resultCode.code();
this.message = resultCode.message();
}

public BizException(ResultCodeEnum resultCode, Throwable cause) {
super(resultCode.message(), cause);
this.code = resultCode.code();
this.message = resultCode.message();
}

public BizException(String message) {
super(message);
this.code = -1;
this.message = message;
}

public BizException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}

public BizException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.message = message;
}

@Override
public synchronized Throwable fillInStackTrace() {
return this;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}

@Override
public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

自定义注解

添加了该注解的Controller的方法的返回结果将被我们统一包装

ResponseResult
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ResponseResult {

}

注解拦截器

创建自定义的拦截器,判断方法是否使用了@ResponseResult

@Component
public class ResponseResultInterceptor implements HandlerInterceptor {
//标记名称
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// TODO Auto-generated method stub
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
// 判断是否在类对象上添加了注解
if (clazz.isAnnotationPresent(ResponseResult.class)) {
// 设置此请求返回体,需要包装,往下传递,在ResponseBodyAdvice接口进行判断
request.setAttribute(RESPONSE_RESULT_ANN, clazz.getAnnotation(ResponseResult.class));
} else if (method.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(RESPONSE_RESULT_ANN, method.getAnnotation(ResponseResult.class));
}
}
return true;
}
}

返回结果处理

使用@ControllerAdvice可以拦截Controller方法返回参数,自动包装成为统一返回结果:

ResponseResultHandler
/**
* 使用 @ControllerAdvice & ResponseBodyAdvice
* 拦截Controller方法默认返回参数,统一处理返回值/响应体
*/
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {

// 标记名称
public static final String RESPONSE_RESULT_ANN = "RESPONSE-RESULT-ANN";


// 判断是否要执行 beforeBodyWrite 方法,true为执行,false不执行,有注解标记的时候处理返回值
@Override
public boolean supports(MethodParameter arg0, Class<? extends HttpMessageConverter<?>> arg1) {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
// 判断请求是否有包装标记
ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANN);
return responseResultAnn == null ? false : true;
}


// 对返回值做包装处理,如果属于异常结果,则需要再包装
@Override
public Object beforeBodyWrite(Object body, MethodParameter arg1, MediaType arg2,
Class<? extends HttpMessageConverter<?>> arg3, ServerHttpRequest arg4, ServerHttpResponse arg5) {
if (body instanceof ErrorResult) {
ErrorResult error = (ErrorResult) body;
return R.fail(error.getCode(), error.getMessage());
} else if (body instanceof R) {
return (R) body;
} else if (body instanceof String) {
return body;
}
return R.success(body);
}
}

全局异常处理

正常返回的结果R被自动包装,那么异常结果也应该被自动包装,因此使用@RestControllerAdvice注解做全局的异常处理,返回错误结果ErrorResult

GlobalExceptionHandler
/**
* 全局异常处理类
* @RestControllerAdvice(@ControllerAdvice),拦截异常并统一处理
* @author sw-code
*
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger("GlobalExceptionHandler.class");

/**
* 处理自定义的业务异常
* @param e 异常对象
* @param request request
* @return 错误结果
*/
@ExceptionHandler(BizException.class)
public ErrorResult bizExceptionHandler(BizException e, HttpServletRequest request) {
logger.error("发生业务异常!原因是: {}", e.getMessage());
return ErrorResult.fail(e.getCode(), e.getMessage());
}

// 拦截抛出的异常,@ResponseStatus:用来改变响应状态码
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Throwable.class)
public ErrorResult handlerThrowable(Throwable e, HttpServletRequest request) {
logger.error("发生未知异常!原因是: ", e);
// 使用这种方式,而不是拦截AccessDeniedException异常,可引入Spring Security防止污染工程
if (e.getMessage().equals("不允许访问")) {
return ErrorResult.fail(ResultCodeEnum.NO_OPERATOR_AUTH, e);
}
return ErrorResult.fail(ResultCodeEnum.SERVER_ERROR, e);
}

// 参数校验异常
@ExceptionHandler(BindException.class)
public ErrorResult handleBindExcpetion(BindException e, HttpServletRequest request) {
logger.error("发生参数校验异常!原因是:",e);
ErrorResult error = ErrorResult.fail(ResultCodeEnum.PARAM_INVALID, e, e.getAllErrors().get(0).getDefaultMessage());
return error;
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
logger.error("发生参数校验异常!原因是:",e);
ErrorResult error = ErrorResult.fail(ResultCodeEnum.PARAM_INVALID,e,e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return error;
}
}

系统如何处理异常?

使用控制器增强注解@RestControllerAdvice捕获处理不同的异常,可以返回一个自定义的异常结果。同时还可以使用增强注解@ControllerAdvice拦截Controller方法的返回,并在beforeBodyWrite方法包装对象和异常结果为统一结果。

自动装配

为了让引入base模块的项目能够自动装配配置类,需要在resources下创建目录META-INF并在目录下创建文件spring.factories,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
com.swx.base.config.LocalDateTimeConfig, \
com.swx.base.config.Knife4jConfiguration, \
com.swx.base.config.WebAppConfig, \
com.swx.base.exception.GlobalExceptionHandler, \
com.swx.base.exception.ResponseResultHandler

所有的配置类都要在这里声明一下。

JSR303校验

请求参数的合法校验如何做?

使用基于JSR303的校验框架实现,Spring Boot提供了JSR-303的支持,它就是spring-boot-starter-validation,它包括了很多校验规则,只需要在模型类中通过注解指定校验规则,在Controller方法上开启校验。

实现统一校验

引入依赖信息

pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

在实体类中加入校验信息,如下:

@Data
@ApiModel(value = "AddCourseDTO", description = "新增课程基本信息")
public class AddCourseDTO {

@NotEmpty(message = "课程名称不能为空")
@ApiModelProperty(value = "课程名称", required = true)
private String name;

@NotEmpty(message = "适用人群不能为空")
@Size(message = "适用人群内容过少", min = 10)
@ApiModelProperty(value = "适用人群", required = true)
private String users;
}

分组校验

修改和新增的校验规则不同,此时可以使用分组来解决。

在base模块的com.swx.base.exception包下创建分组类:

ValidationGroup
/**
* 用于分组校验
*/
public class ValidationGroup {
public interface Insert{};
public interface Update{};
public interface Delete{};
}

修改校验规则

@NotEmpty(message = "新增课程名称不能为空", groups = {ValidationGroup.Insert.class})
@NotEmpty(message = "修改课程名称不能为空", groups = {ValidationGroup.Update.class})
@ApiModelProperty(value = "课程名称", required = true)
private String name;

增加分组信息

@ApiOperation("新增课程")
@PostMapping("")
public CourseBaseInfoVO createCourseBase(@RequestBody @Validated(ValidationGroup.Insert.class) AddCourseDTO dto) {
Long companyId = 1232141425L;
return courseBaseService.createCourseBase(companyId, dto);
}