单体项目搭建套路

赖卓成2022年11月9日
大约 7 分钟

项目搭建

每个项目基本都能用上的依赖:

  • 继承父工程、启动器

        <parent>
            <artifactId>spring-boot-starter-parent</artifactId>
            <groupId>org.springframework.boot</groupId>
            <version>2.7.5</version>
        </parent>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
    
  • JSR380

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

    自定义注解:

    @Documented
    @Constraint(validatedBy = MediasFieldIdValidator.class)  // 指定校验器
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ValidMediasFieldId {
        String message() default "fieldld字段:type(媒体类型)为`VIDEO`时必填;6~64个字符电长度";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    }
    
    
    public class MediasFieldIdValidator implements ConstraintValidator<ValidMediasFieldId, NewsAddReq.NewsMedias> {
    
        @Override
        public boolean isValid(NewsAddReq.NewsMedias newsMedias, ConstraintValidatorContext constraintValidatorContext) {
            if (newsMedias.getType().equals("VIDEO")&& StrUtil.isBlank(newsMedias.getFieldId())){
                return false;
            }
            return true;
        }
    }
    
    

    复杂类型多层嵌套时,需要都打上注解,例如:

    /**
     * 添加新闻请求体
     */
    @Data
    public class NewsAddReq {
    
    
        /**
         * 用户信息
         */
        @NotNull(message = "用户信息不能为空")
        @Valid
        private Author author;
    
        /**
         * 标签列表
         */
        private List<String> tags;
    
        /**
         * 新闻内容等信息
         */
        @Valid
        private NewsBody body;
    
    
        /**
         * 用户信息
         */
        @Data
        public static class Author{
            /**
             * 用户昵称
             */
            @NotBlank(message = "用户昵称不可为空")
            private String nickname;
    
            /**
             * 用户头像
             */
            private String avatar;
    
    
        }
    
        /**
         * body信息
         */
        @Data
        public static class NewsBody{
            /**
             * 新闻内容
             */
            @NotBlank(message = "新闻内容不可为空")
            private String content;
    
            /**
             * 媒体列表
             */
            @Valid
            private List<NewsMedias> medias;
        }
    
        /**
         * 媒体信息
         */
        @Data
        @ValidMediasFieldId
        public static class NewsMedias{
            /**
             * 媒体类型
             */
            @NotBlank(message = "媒体类型不可为空")
            @ValidMediaType(message = "媒体类型必须为VIDEO或IMAGE")
            private String type;
    
            /**
             *  媒体url地址
             */
            @NotBlank(message = "媒体url地址不可为空")
            private String url;
    
            /**
             * type(媒体类型)为`VIDEO`时必填;6~64个字符电长度
             */
            private String fieldId;
        }
    }
    
    
  • hutool

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.41</version>
    </dependency>
    
  • mysql或sqlserver,其中sqlserver经常连不上,目前稳定使用的版本:8.2.2.jre8

            <dependency>
                <groupId>com.microsoft.sqlserver</groupId>
                <artifactId>mssql-jdbc</artifactId>
                <version>8.2.2.jre8</version>
            </dependency>
    
            <dependency>
              <groupId>com.mysql</groupId>
              <artifactId>mysql-connector-j</artifactId>
              <version>8.3.0</version>
          </dependency>
    
  • druid连接池,jdbc,mybatis-plus

            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.2.14</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.5.2</version>
            </dependency>
    
  • mybatis-plus自动维护字段更新

    自定义元对象处理器

    /**
     * 自定义元对象处理器
     * 自动填充创建时间和更新时间
     */
    @Slf4j
    public class CustomMetaObjectHandler implements MetaObjectHandler {
    
        /**
         * 插入时自动填充
         */
        @Override
        public void insertFill(MetaObject metaObject) {
            log.info("开始插入填充...");
            
            // 填充创建时间
            this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
            this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
            
            // 填充更新时间(插入时与创建时间相同)
            this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
            this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
            
            // 如果需要填充创建人和更新人,可以在这里设置
             this.strictInsertFill(metaObject, "createBy", String.class, getCurrentUserId());
             this.strictInsertFill(metaObject, "updateBy", String.class, getCurrentUserId());
        }
    
        /**
         * 更新时自动填充
         */
        @Override
        public void updateFill(MetaObject metaObject) {
            log.info("开始更新填充...");
            
            // 填充更新时间
            this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
            this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
            
            // 如果需要填充更新人,可以在这里设置
            // this.strictUpdateFill(metaObject, "updateBy", String.class, getCurrentUserId());
        }
        
        /**
         * 获取当前用户ID(需要根据您的认证系统实现)
         */
        private String getCurrentUserId() {
            // 这里需要根据您的用户认证系统来获取当前用户ID
            // 例如从Spring Security上下文或JWT token中获取
             return "system"; // 默认值
    
        }
    }
    

    配置类:

    /**
     * MyBatis-Plus 配置类
     */
    @Configuration
    public class MyBatisPlusConfig {
    
        /**
         * 分页插件
         */
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
            return interceptor;
        }
    
        /**
         * 自动填充配置
         */
        @Bean
        public MetaObjectHandler metaObjectHandler() {
            return new CustomMetaObjectHandler();
        }
    }
    

    实体类需要加上注解,例如:

    /**
     * 基础实体类
     * 包含公共字段
     */
    @Data
    public class BaseEntity {
    
        /**
         * 创建人
         */
        @TableField(fill = FieldFill.INSERT)
        private String createBy;
    
        /**
         * 创建时间
         */
        @TableField(fill = FieldFill.INSERT)
        private Date createTime;
    
        /**
         * 更新人
         */
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private String updateBy;
    
        /**
         * 更新时间
         */
        @TableField(fill = FieldFill.INSERT_UPDATE)
        private Date updateTime;
    
        /**
         * 逻辑删除
         */
        private Integer deleted;
    }
    
  • mybatis-plus业务层和持久层写法举例:

    service接口

    /**
     * 用户表Service接口
     */
    public interface UserService extends IService<User> {
    }
    

    service实现:

    /**
     * 用户表Service实现类
     */
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    }
    

    mapper接口:

    /**
     * 用户表Mapper接口
     */
    @Mapper
    public interface UserMapper extends BaseMapper<User> {
        
    }
    

    mapper.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="org.lzc.mapper.UserMapper">
    
    </mapper>
    
  • swagger

            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-boot-starter</artifactId>
                <version>3.0.0</version>
            </dependency>
    
  • 打包插件和多环境配置

        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
        <profiles>
            <profile>
                <id>dev</id>
                <activation>
                    <activeByDefault>true</activeByDefault>
                </activation>
                <properties>
                    <profile.name>dev</profile.name>
                </properties>
            </profile>
    
            <profile>
                <id>prod</id>
                <properties>
                    <profile.name>prod</profile.name>
                </properties>
            </profile>
        </profiles>
    
  • 多环境配置,一个application.yml,其他的加后缀。

    server:
      port: 8030
    
    spring:
      profiles:
        active: dev
      mvc:
        pathmatch:
          matching-strategy: ant_path_matcher
    
  • 数据源配置:

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://39.108.116.255:3306/gd?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        type: com.alibaba.druid.pool.DruidDataSource
        username: root
        password: qq1111.1
        druid:
          # 初始化连接池大小
          initial-size: 10
          max-active: 150
          ## 开启基础监控
          stat-view-servlet:
            enabled: true
            # 账号密码
            login-username: root
            login-password: qq1111.1
            # 访问路径:ip:port/druid/login.html
            url-pattern: /druid/*
            # ip白名单(没有配置为空,则允许所有访问)。deny是设置黑名单
            allow: null
          filter:
            # 开启sql监控
            stat:
              enabled: true
              db-type: sqlserver
              # 慢sql监控,sql监控,最慢会显示红色
              log-slow-sql: true
              slow-sql-millis: 200
            # 开启SQL防火墙监控
            wall:
              enabled: true
              db-type: sqlserver
              config:
                # 是否允许删除
                delete-allow: false
                # 是否允许删除表
                drop-table-allow: false
              # 开启日志打印
            slf4j:
              enabled: true
              statement-prepare-after-log-enabled: false
              statement-close-after-log-enabled: false
          # 开启web应用和URI监控
          web-stat-filter:
            enabled: true
            # 排除的
            exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,/swagger-resources*,/webjars*,/v2*,/doc.html"
            # profileEnable能够监控单个url调用的sql列表
            profileEnable: true
            # sessionStatEnable 开启session监控
    
    

不用druid可以简化一下:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://39.108.116.255:3306/gd?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: qq1111.1
knife4j:
  enable: true
  openapi:
    title: Knife4j官方文档
    description: "`我是测试`,**你知道吗**
    # aaa"
    email: xiaoymin@foxmail.com
    concat: 八一菜刀
    url: https://docs.xiaominfo.com
    version: v4.0
    license: Apache 2.0
    license-url: https://stackoverflow.com/
    terms-of-service-url: https://stackoverflow.com/
    group:
      test1:
        group-name: 分组名称
        api-rule: package
        api-rule-resources:
          - org.lzc.rest
  • 接口文档:

    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
        <version>4.4.0</version>
    </dependency>
    
    knife4j:
      enable: true
      openapi:
        title: Knife4j官方文档
        description: "`我是测试`,**你知道吗**
        # aaa"
        email: xiaoymin@foxmail.com
        concat: 八一菜刀
        url: https://docs.xiaominfo.com
        version: v4.0
        license: Apache 2.0
        license-url: https://stackoverflow.com/
        terms-of-service-url: https://stackoverflow.com/
        group:
          test1:
            group-name: 分组名称
            api-rule: package
            api-rule-resources:
              - com.knife4j.demo.new3
    

配置和规范

  • 允许跨域:

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("*")
                    .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                    .maxAge(3600)
                    .allowedHeaders("*")
                    .allowCredentials(false);
        }
        
    
        @Bean
        public RestTemplate restTemplate(){
            return new RestTemplate();
        }
    }
    
  • 统一返回结果

    /**
     * 统一返回结果
     *
     * @author laizhuocheng
     * @date 2022/11/09
     */
    @Data
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public class Result<T> implements Serializable {
    
        private static final long serialVersionUID = 2405172041950251807L;
    
        /**
         * 返回编码
         */
        private Integer code;
    
        /**
         * 编码描述
         */
        private String msg;
    
        /**
         * 业务数据
         */
        private T data;
    
        public static <T> Result<T> success(T data) {
            Result<T> result = new Result<>();
            result.setCode(0);
            result.setMsg("success");
            result.setData(data);
            return result;
        }
    
        public static <T> Result<T> success(Integer code ,String msg,T data) {
            Result<T> result = new Result<>();
            result.setCode(code);
            result.setMsg(msg);
            result.setData(data);
            return result;
        }
    }
    
    

    使用注解,就不用每次return都Result.ok()之类的:

    /**
     * 统一返回结果注解
     *
     * @author laizhuocheng
     * @date 2022/11/09
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ApiResult {
        String value() default "";
    
        int successCode() default 0;
    
        String successMsg() default "success";
    
        Class<? extends Result> resultClass() default Result.class;
    }
    
    /**
     * RestController增强
     *
     * @author laizhuocheng
     * @date 2022/11/09
     */
    
    @Order(0)
    @ControllerAdvice
    public class MyResponseBodyAdvice implements ResponseBodyAdvice {
    
        protected boolean isStringConverter(Class converterType) {
            return converterType.equals(StringHttpMessageConverter.class);
        }
    
        protected boolean isApiResult(MethodParameter returnType) {
            return returnType.hasMethodAnnotation(ApiResult.class);
        }
    
        @Override
        public boolean supports(MethodParameter returnType, Class converterType) {
            return !isStringConverter(converterType) && isApiResult(returnType);
        }
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                      Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            //关键
            return Result.success(body);
        }
    
    }
    
  • 全局异常捕获

    /**
     * 全局异常处理
     *
     * @author laizhuocheng
     * @date 2022/11/09
     */
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
        public Object methodArgumentNotValidHandler(HttpServletRequest request, Exception  e) {
            e.printStackTrace();
            BindingResult bindingResult;
            if (e instanceof MethodArgumentNotValidException) {
                bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
            } else {
                bindingResult = ((BindException) e).getBindingResult();
            }
            FieldError fieldError = bindingResult.getFieldError();
            return Result.success(BusinessExceptionEnum.PARAMETERS_ERROR.getCode(), BusinessExceptionEnum.PARAMETERS_ERROR.getMsg()
                    +(null!=fieldError?"[" + fieldError.getField() + "]" + fieldError.getDefaultMessage():""),null);
        }
    
        @ExceptionHandler(value = BusinessException.class)
        public Object businessExceptionHandler(HttpServletRequest request, BusinessException e) {
            e.printStackTrace();
            return Result.success(e.getCode(), e.getMessage(),null);
        }
    
        @ExceptionHandler(value = Exception.class)
        public Object defaultErrorHandler(HttpServletRequest request, Exception e) {
            e.printStackTrace();
            return Result.success(BusinessExceptionEnum.UN_KNOW_ERROR.getCode(), BusinessExceptionEnum.UN_KNOW_ERROR.getMsg(),null);
        }
    }
    
    
    @Data
    @EqualsAndHashCode(callSuper = false)
    public class BusinessException extends RuntimeException{
    
    
        private Integer code;
    
        private  String message;
    
        public BusinessException() {
            super();
        }
    
        public BusinessException(String message) {
            super(message);
            this.message = message;
        }
    
        public BusinessException(Integer code, String message) {
            super(message);
            this.code = code;
            this.message = message;
        }
    
        public BusinessException(BusinessExceptionEnum exceptionEnum) {
            super(exceptionEnum.getMsg());
            this.code = exceptionEnum.getCode();
            this.message = exceptionEnum.getMsg();
        }
    
    }
    
    /**
     * 业务异常枚举
     *
     * @author laizhuocheng
     * @date 2022/10/31
     */
    @Getter
    @AllArgsConstructor
    public enum BusinessExceptionEnum {
    
        /**
         * 未知错误
         */
        UN_KNOW_ERROR(-1,"服务器开小差了~"),
    
        /**
         * 失败
         */
        ERROR(1,"失败"),
    
        /**
         * 参数校验错误
         */
        PARAMETERS_ERROR(0,"参数校验错误"),
    
        /**
         * 未查询到相关信息
         */
        NOT_FOUND(50001,"未查询到相关信息");
    
    
    
        private final Integer code;
        
        private final String msg;
    
    }
    
  • 拦截器校验登录状态

    public class AuthInterceptor implements AsyncHandlerInterceptor {
    
        private ObjectMapper objectMapper = new ObjectMapper();
    
        @Autowired
        private ClientCorpMapper clientCorpMapper;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            String token = request.getHeader("token");
            if (StringUtils.isEmpty(token)){
                responseData(response,BusinessExceptionEnum.UN_AUTHORIZATION);
                return false;
            }
    
            UserClaimDTO userClaimDTO = JwtUtil.parseToken(token, UserClaimDTO.class);
            if (null==userClaimDTO){
                responseData(response,BusinessExceptionEnum.UN_AUTHORIZATION);
                return false;
            }
            // 获取用户信息
            ClientCorp clientCorp = clientCorpMapper.selectOne(new LambdaQueryWrapper<ClientCorp>().eq(ClientCorp::getCorpGUID, userClaimDTO.getId()));
            if (null==clientCorp){
                responseData(response,BusinessExceptionEnum.USER_DOES_NOT_EXIST);
                return false;
            }
    
            UserThreadLocalContext.set(clientCorp);
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            AsyncHandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            AsyncHandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        }
    
        private void responseData(HttpServletResponse response, BusinessExceptionEnum businessExceptionEnum)
                throws IOException {
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Content-Type", "application/json;charset=utf-8");
            response.getWriter().write(Objects.requireNonNull(objectMapper.writeValueAsString(Result.success(businessExceptionEnum.getCode()
                    ,businessExceptionEnum.getMsg(),null))));
        }
    }
    
    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins("*")
                    .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                    .maxAge(3600)
                    .allowedHeaders("*")
                    .allowCredentials(false);
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // TODO
            InterceptorRegistration registration = registry.addInterceptor(new AuthInterceptor());
            registration.addPathPatterns("/**");
            registration.excludePathPatterns("/**/error","/swagger-ui/**","/corp/sso/**");
        }
    
    
        @Bean
        public RestTemplate restTemplate(){
            return new RestTemplate();
        }
    }
    
    
Loading...