[springboot] 详述springboot通用开发架构

springboot

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.javaDeleteRequest.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这样的系统错误。**从而让开发人员对于业务逻辑错误有更适当的处理

例如,如果在购物车应用中,用户尝试购买一个不存在的商品,这可以抛出一个继承自RuntimeExceptionProductNotFoundException异常。然后开发人员就可以根据这个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.javaLogInterceptor.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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程就是n踢r

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值