单体项目搭建套路
2022年11月9日
项目搭建
每个项目基本都能用上的依赖:
继承父工程、启动器
<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...
