目录
springboot 通用架构
annotation
├─ 📁annotation
│ └─ 📄AuthCheck.java
annotation
: 存放自定义注解的目录,这里有一个 AuthCheck.java
注解,可能用于权限控制。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {
/**
* 必须有某个角色
*
* @return
*/
String mustRole() default "";
}
权限控制注解类,被@AuthCheck标记的类要mustRole
下面是一个@AuthCheck的用例
@PostMapping("/delete")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)//mustRole必须是ADMIN_ROLE(管理员权限)
public BaseResponse<Boolean> deleteUser(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
if (deleteRequest == null || deleteRequest.getId() <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
boolean b = userService.removeById(deleteRequest.getId());
return ResultUtils.success(b);
}
上面这个deleteUser这个controller被加上@AuthCheck注解,只有当前用户是ADMIN_ROLE才可以调用该接口,否则就报权限不足错误
NO_AUTH_ERROR(40101, "无权限"),
Common
存放通用的类文件,包括一些基础响应类 BaseResponse.java
,错误码定义 ErrorCode.java
,分页请求类 PageRequest.java
,以及一些通用的工具类 ResultUtils.java
和 DeleteRequest.java
。
├─ 📁common
│ ├─ 📄BaseResponse.java
│ ├─ 📄ErrorCode.java
│ ├─ 📄PageRequest.java
│ └─ 📄ResultUtils.java
BaseResponse
下面是基础响应类,它定义了每个响应都具有的code(状态码),data(泛型返回数据对象),message等。
//BaseResponse.java
@Data
public class BaseResponse<T> implements Serializable {
private int code;//状态码
private T data;//泛型返回数据对象
private String message;//提示消息
public BaseResponse(int code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
}
//默认有参构造函数
public BaseResponse(int code, T data) {
this(code, data, "");
}
//根据ErrorCode枚举 构造函数
public BaseResponse(ErrorCode errorCode) {
this(errorCode.getCode(), null, errorCode.getMessage());
}
}
ErrorCode是定义错误码的eumn类,每个错误都有自己错误的状态码(401?500?),提示信息(资源不存在?服务器错误?)等等。下面会介绍
ErrorCode
ErrorCode定义了不同错误的枚举,每个枚举都拥有自己的code,message,这将作为baseResponse 企图返回的错误。
public enum ErrorCode {
SUCCESS(0, "ok"),
PARAMS_ERROR(40000, "请求参数错误"),
NOT_LOGIN_ERROR(40100, "未登录"),
NO_AUTH_ERROR(40101, "无权限"),
NOT_FOUND_ERROR(40400, "请求数据不存在"),
FORBIDDEN_ERROR(40300, "禁止访问"),
SYSTEM_ERROR(50000, "系统内部异常"),
OPERATION_ERROR(50001, "操作失败");
/**
* 状态码
*/
private final int code;
/**
* 信息
*/
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
ResultUtils
ResultUtils类 为“结果工具类”,类如其名,它采用static的方式,负责对最后响应的data基于baseResponse进行的封装。
public class ResultUtils {
/**
* 成功
*
* @param data
* @param <T>
* @return
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0, data, "ok");
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}
/**
* 失败
*
* @param code
* @param message
* @return
*/
public static BaseResponse error(int code, String message) {
return new BaseResponse(code, null, message);
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode, String message) {
return new BaseResponse(errorCode.getCode(), null, message);
}
}
Q:为什么要static呢?不直接new一个baseResponse返回呢?
A:因为static只静态加载一次类,而你的new在堆上一直重复创建对象和删除。
的
Q:为什么success采用了泛型的写法,而error没有呢?
A:因为success需要封装一个data并返回,而error没有。所以对于error来说,你的泛型是用不上的,虽然idea会报黄(因为baseResponse是的),但其实没有关系
PageRequest
如果你经常用mybatis-plus,那么分页这个功能一定不陌生。
比起用mybatis-plus的page接口,这里定义了PageRequest来对分页功能做更多的说明。
PageRequest封装了关于分页的一些参数,包括一些currentPage(当前页号),PageSize页面大小),sort排序方式和排序字段等。
@Data
public class PageRequest {
/**
* 当前页号
*/
private long current = 1;
/**
* 页面大小
*/
private long pageSize = 10;
/**
* 排序字段
*/
private String sortField;
/**
* 排序顺序(默认升序)
*/
private String sortOrder = CommonConstant.SORT_ORDER_ASC;
}
其他DTO对象就可以通过extend PageRequest,来解锁“分页”的功能
下面是一个小例子:
比如我们定义了一个userQueryRequest,它将作为封装查询user的DTO对象
@EqualsAndHashCode(callSuper = true)
@Data
public class UserQueryRequest extends PageRequest implements Serializable {
/**
* id
*/
private Long id;
/**
* 用户昵称
*/
private String userName;
/**
* 简介
*/
private String userProfile;
/**
* 用户角色
*/
private String userRole;
private static final long serialVersionUID = 1L;
}
ok,现在在userController中,我们可以看看如果想分页查询user数据,应该是怎么样的?
@PostMapping("/list/page")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<User>> listUserByPage(@RequestBody UserQueryRequest userQueryRequest,
HttpServletRequest request) {
//获取当前页
long current = userQueryRequest.getCurrent();
//获取页面大小
long size = userQueryRequest.getPageSize();
//然后调用mybatis-plus的userService的page方法直接查询。
Page<User> userPage = userService.page(new Page<>(current, size),
userService.getQueryWrapper(userQueryRequest));
//最后通过ResultUtils去将page<User>对象 返回
return ResultUtils.success(userPage);
}
通过进一步封装,让分页查询更加模块化了,到时候如果想改size和current,直接去PageRequest中修改,其他使用了分页功能的模块就能全局自动修改。
config
存放配置类,我们这里包括跨域配置 CorsConfig.java
、CosClient配置 CosClientConfig.java
、JSON配置 JsonConfig.java
、Knife4j配置 Knife4jConfig.java
、MyBatis Plus配置 MyBatisPlusConfig.java
和微信开放平台配置 WxOpenConfig.java
。
constant
存放常量类,我们这里包括通用常量 CommonConstant.java
、文件相关的常量 FileConstant.java
以及用户相关的常量 UserConstant.java
。
!model !
存放数据模型类,包括实体类目录 entity
,枚举类目录 enums
和值对象类目录 vo
,以及数据传输对象类目录 dto
。
entity也就是常说的domain,二者一样,(必须有)
enums枚举类是在model里面的枚举,可以不写
dto:数据传输对象,比如将前端请求的一些参数转换为dto对象,让java程序更加模块化地执行业务。
vo:视图对象,最终返回给前端视图的对象,一般是对baseResponse响应的对象做一些处理,让它可以被前端更好更方便地处理(比如用户脱敏操作,你后端不做前端就得做)
entity
下面是user表(关系型数据库)对应在java中的对象(符合javaBean)
@TableName(value = "user")
@Data
public class User implements Serializable {
/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户账号
*/
private String userAccount;
/**
* 用户密码
*/
private String userPassword;
/**
* 用户昵称
*/
private String userName;
/**
* 用户头像
*/
private String userAvatar;
/**
* 用户简介
*/
private String userProfile;
/**
* 用户角色
*/
private String userRole;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 是否删除
*/
@TableLogic
private Integer isDelete;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
dto
下面是user如何在dto的例子
─ 📁dto
└─ 📁user
├─ 📄UserAddRequest.java
├─ 📄UserLoginRequest.java
├─ 📄UserQueryRequest.java
├─ 📄UserRegisterRequest.java
└─ 📄UserUpdateRequest.java
dto demo1
举一个例子:比如UserAddRequest.java,类如其名,用户添加请求,
所以它里面定义的都是“我要添加一个用户,我需要些什么请求字段?”
@Data
public class UserAddRequest implements Serializable {
/**
* 用户昵称
*/
private String userName;
/**
* 账号
*/
private String userAccount;
/**
* 用户头像
*/
private String userAvatar;
/**
* 用户角色: user, admin
*/
private String userRole;
private static final long serialVersionUID = 1L;
}
如上所示,添加一个user,我们需要填充他的userName,userAccount,userAvatar等等,这里的字段就是前端的一些表单之类所要的。
Q:为什么没有id呢?
A:因为id由后端自动生成,然后插入数据库中。
所以前端发出一个add请求时不需要id,只要用户的其他信息
dto demo2
再比如UserQueryRequest.java,类如其名,用户查询请求封装类
也就是说 查询一个用户,前端发来的请求字段应该是些什么?
@EqualsAndHashCode(callSuper = true)
@Data
public class UserQueryRequest extends PageRequest implements Serializable {
/**
* id
*/
private Long id;
/**
* 用户昵称
*/
private String userName;
/**
* 简介
*/
private String userProfile;
/**
* 用户角色
*/
private String userRole;
private static final long serialVersionUID = 1L;
}
如上所示,貌似和user(entity)差不多哇,
但其实我们在进行用户查询时,可能按id查,也可能按昵称查,也可能按照简介查询(模糊匹配)。
Q:如果我大部分情况都只是按userName查呢?定义这么多是不是有点重复?
A:事实上是重复的,确实很多情况并不是多个条件的查询。
但是我们可以在service层封装我们的一些beanUtils工具包,只将该UserQueryRequest对象中不为null的属性作为查询字段来给到我们的sql,实操起来这样也很方便。
并且这样的写法可以说囊括了一切情况,你要按什么查就传什么参数就行,后端代码基本不动。更符合DDD的模块设计。
vo
view Object,就是 传给视图的封装对象
白话:后端应该传给前端怎样的响应结果,让其更方便地展示在视图中。
下面是vo的一个实际例子
vo demo
下面是userVo类的定义,里面包含了一些user在view层的封装,以及entity转vo,vo转entity等静态方法。
@Data
public class UserVO implements Serializable {
/**
* id
*/
private Long id;
/**
* 用户昵称
*/
private String userName;
/**
* 用户头像
*/
private String userAvatar;
/**
* 用户简介
*/
private String userProfile;
/**
* 用户角色:user/admin/ban
*/
private String userRole;
/**
* 创建时间
*/
private Date createTime;
private final static Gson GSON = new Gson();
/** objToVo
* @author xsr
* 作用:普通user对象转userVo对象
*/
public static UserVO objToVo(User user){
if (user == null) {
return null;
}
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
return userVO;
}
/** voToObj
* @author xsr
* 作用:将userVo转为user
*/
public static User voToObj(UserVO userVO){
if (userVO == null) {
return null;
}
User user = new User();
BeanUtils.copyProperties(userVO, user);
return user;
}
private static final long serialVersionUID = 1L;
}
如上所示,我们设定前端可以展示user的创建时间,而不是更新时间。
即使user对象同时有这俩个属性,但我们只让创建时间传给前端。这样前端在处理数据时就更加方便,更加模块化。
至于entity转vo,vo转entity等静态方法
这些将在service层业务逻辑处理时,为程序员方便于后端内部不同数据模型的相互转换。
Controller,service,mapper
作为spring MVC设计思想的一员
exception
异常处理在任何语言,任何框架中都是非常重要的一环。
这里java的springboot通用框架中,我们定义了exception包存来放异常处理相关的类,包括自定义异常类 BusinessException.java
、全局异常处理器 GlobalExceptionHandler.java
和异常处理工具类 ThrowUtils.java
。
下面是exception目录树
├─ 📁exception
│ ├─ 📄BusinessException.java
│ ├─ 📄GlobalExceptionHandler.java
│ └─ 📄ThrowUtils.java
BusinessException
BusinessException是业务逻辑错误的异常类。下面是具体定义代码
/**
* 自定义异常类
*
*/
public class BusinessException extends RuntimeException {
/**
* 错误码
*/
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
public int getCode() {
return code;
}
}
如上所示,BusinessException继承了RuntimeException,以使其可以作为一个异常声明却可以不强制地捕获(不是系统错误)。
它清晰地告诉了开发人员:**只要业务逻辑错误都应该抛出这个BusinessException错误,而不是java SDK中RuntimeException这样的系统错误。**从而让开发人员对于业务逻辑错误有更适当的处理
例如,如果在购物车应用中,用户尝试购买一个不存在的商品,这可以抛出一个继承自
RuntimeException
的ProductNotFoundException
异常。然后开发人员就可以根据这个ProductNotFoundException(就是BusinessException)来进行相应的逻辑处理:比如日志记录,报告系统,提醒用户类似于“这是个不存在商品,你需要重新购买”这样的异常业务逻辑
GlobalExceptionHandler
GlobalExceptionHandler类,类如其名,全局异常处理器。
在这个类中对各种各样的异常Exception类进行捕获处理,开发人员一般就可以在这里对于不同情况的异常做不同处理。
下面是GlobalExceptionHandler示例代码:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public BaseResponse<?> businessExceptionHandler(BusinessException e) {
//发生BusinessException异常 这时应该做点什么.....
log.error("BusinessException", e);
return ResultUtils.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {
//发生最基础的 RuntimeException 异常 这时应该做点什么.....
log.error("RuntimeException", e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
}
}
如上所示,在GlobalExceptionHandler中,根据不同 **@ExceptionHandler(xxx.class)**注解来对xxx异常做处理,比如日志记录,反馈系统,业务处理等。
ThrowUtils
ThrowUtils并不是很通用,这里不做具体介绍
下面是实例代码:
/**
* 抛异常工具类
*
*/
public class ThrowUtils {
/**
* 条件成立则抛异常
*
* @param condition
* @param runtimeException
*/
public static void throwIf(boolean condition, RuntimeException runtimeException) {
if (condition) {
throw runtimeException;
}
}
/**
* 条件成立则抛异常
*
* @param condition
* @param errorCode
*/
public static void throwIf(boolean condition, ErrorCode errorCode) {
throwIf(condition, new BusinessException(errorCode));
}
/**
* 条件成立则抛异常
*
* @param condition
* @param errorCode
* @param message
*/
public static void throwIf(boolean condition, ErrorCode errorCode, String message) {
throwIf(condition, new BusinessException(errorCode, message));
}
}
aop
├─ 📁aop
│ ├─ 📄AuthInterceptor.java
│ └─ 📄LogInterceptor.java
存放切面类的目录,包括 AuthInterceptor.java
和 LogInterceptor.java
,用于实现切面编程,比如日志记录和权限检查。
下面是AuthInterceptor:
@Aspect
@Component
public class AuthInterceptor {
@Resource
private UserService userService;
/**
* 执行拦截
* @param joinPoint
* @param authCheck
* @return
*/
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
String mustRole = authCheck.mustRole();
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 当前登录用户
User loginUser = userService.getLoginUser(request);
// 必须有该权限才通过
if (StringUtils.isNotBlank(mustRole)) {
UserRoleEnum mustUserRoleEnum = UserRoleEnum.getEnumByValue(mustRole);
if (mustUserRoleEnum == null) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
String userRole = loginUser.getUserRole();
// 如果被封号,直接拒绝
if (UserRoleEnum.BAN.equals(mustUserRoleEnum)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
// 必须有管理员权限
if (UserRoleEnum.ADMIN.equals(mustUserRoleEnum)) {
if (!mustRole.equals(userRole)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
}
}
// 通过权限校验,放行
return joinPoint.proceed();
}
}
如上所示,AuthInterceptor的@Aspect表明这是一个切面类,通过@authCheck注解的doInterceptor来处理用户权限的问题。
下面是LogInterceptor:
@Aspect
@Component
@Slf4j
public class LogInterceptor {
/**
* 执行拦截
*/
@Around("execution(* com.yupi.yupi_api_backed.controller.*.*(..))")
public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {
// 计时
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 获取请求路径
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
// 生成请求唯一 id
String requestId = UUID.randomUUID().toString();
String url = httpServletRequest.getRequestURI();
// 获取请求参数
Object[] args = point.getArgs();
String reqParam = "[" + StringUtils.join(args, ", ") + "]";
// 输出请求日志
log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url,
httpServletRequest.getRemoteHost(), reqParam);
// 执行原方法
Object result = point.proceed();
// 输出响应日志
stopWatch.stop();
long totalTimeMillis = stopWatch.getTotalTimeMillis();
log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis);
return result;
}
}
如上,对于权限校验,记录了这些日志信息。
关于application.yml文件
application.yml是项目配置文件
但我们经常需要区分不同环境下的application.yml,比如线上,测试,开发环境。
📁resources
├─ 📄application-prod.yml
├─ 📄application-test.yml
├─ 📄application.yml
https://blog.csdn.net/it_xushixiong/article/details/131455033