构建Springboot服务脚手架
本文介绍了脚手架常用三种方式。以及通过maven archtype搭建一个脚手架和基于脚手架构建项目的流程。
脚手架介绍
为什么要有脚手架
脚手架好处有:
- 统一研发框架
- 提升研发效率,减少搭建项目的时间。
脚手架三种方式
1、搭建一个服务demo
每次需要创建一个新服务项目时,就拉取下这个服务的代码,进行开发。
2、基于maven的archetype
在IDEA中可以直接基于这个构建一个项目。
3、spring boot initialier
基于maven的archetype
新建一个spring boot服务
搭建一个Spring boot的实例。视图层支持json和velocity两种。对于视图模板Jsp/Velocity/Thymeleaf/Freemarker,只能在项目中支持一种,这里采用Velocity模板。
工程概览
项目结构如下图:
配置文件
配置pom.xml
需要注意的如下:
- <packaing>设置为jar
具体内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.harvey</groupId>
<artifactId>springBootTemplate</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<log4j2.version>2.5</log4j2.version>
<mysql-connector.version>8.0.11</mysql-connector.version>
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<hibernate-validator.version>6.1.6.Final</hibernate-validator.version>
<guava.version>17.0</guava.version>
<apache-common3.version>3.2.1</apache-common3.version>
<lombok.version>1.16.10</lombok.version>
<caffeine.version>3.1.5</caffeine.version>
<fastjson2.version>2.0.24</fastjson2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- 视图模板Jsp/Velocity/Thymeleaf/Freemarker -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<!--hibernate-validation-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<!--Caffeine Cache-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${apache-common3.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml
文件内容如下:
server:
port: 9999
spring:
datasource:
#通用配置
driver-class-name: com.mysql.cj.jdbc.Driver
password: 123456
username: root
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&charcterEncoding=UTF-8&useSSL=false
#数据源连接池配置
hikari:
minimum-idle: 10
maximum-pool-size: 20
idle-timeout: 500000
max-lifetime: 540000
connection-timeout: 60000
connection-test-query: select 1
# mybatis-plus配置
mybatis-plus:
# xml扫描,告诉 Mapper 所对应的 XML 文件位置
mapper-locations: classpath*:mapper/**/*Mapper.xml
configuration:
# 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
map-underscore-to-camel-case: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
注意:需要配置数据库信息,如果不配置就会出现“Cannot determine embedded database driver class for database type NONE”。
还有一种方法可以避免出现这个错误,就是在@SpringBootApplication中添加“exclude={DataSourceAutoConfiguration.class})”。
logback-spring.xml
日志配置内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- appender是configuration的子节点,是负责写日志的组件。 -->
<!-- ConsoleAppender:把日志输出到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 默认情况下,每个日志事件都会立即刷新到基础输出流。 这种默认方法更安全,因为如果应用程序在没有正确关闭appender的情况下退出,则日志事件不会丢失。
但是,为了显着增加日志记录吞吐量,您可能希望将immediateFlush属性设置为false -->
<!--<immediateFlush>true</immediateFlush>-->
<encoder>
<!-- %37():如果字符没有37个字符长度,则左侧用空格补齐 -->
<!-- %-37():如果字符没有37个字符长度,则右侧用空格补齐 -->
<!-- %15.15():如果记录的线程字符长度小于15(第一个)则用空格在左侧补齐,如果字符长度大于15(第二个),则从开头开始截断多余的字符 -->
<!-- %-40.40():如果记录的logger字符长度小于40(第一个)则用空格在右侧补齐,如果字符长度大于40(第二个),则从开头开始截断多余的字符 -->
<!-- %msg:日志打印详情 -->
<!-- %n:换行符 -->
<!-- %highlight():转换说明符以粗体红色显示其级别为ERROR的事件,红色为WARN,BLUE为INFO,以及其他级别的默认颜色。 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) --- [%15.15(%thread)] %cyan(%-40.40(%logger{40})) : %msg%n
</pattern>
<!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- info 日志-->
<!-- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<!-- 以下的大概意思是:1.先按日期存日志,日期变了,将前一天的日志文件名重命名为XXX%日期%索引,新的日志仍然是project_info.log -->
<!-- 2.如果日期没有发生变化,但是当前日志的文件大小超过10MB时,对当前日志进行分割 重命名-->
<appender name="info_log" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志文件路径和名称-->
<File>logs/app_info.log</File>
<!--是否追加到文件末尾,默认为true-->
<append>true</append>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch><!-- 如果命中ERROR就禁止这条日志 -->
<onMismatch>ACCEPT</onMismatch><!-- 如果没有命中就使用这条规则 -->
</filter>
<!--有两个与RollingFileAppender交互的重要子组件。 第一个RollingFileAppender子组件,即RollingPolicy:负责执行翻转所需的操作。
RollingFileAppender的第二个子组件,即TriggeringPolicy:将确定是否以及何时发生翻转。 因此,RollingPolicy负责什么和TriggeringPolicy负责什么时候.
作为任何用途,RollingFileAppender必须同时设置RollingPolicy和TriggeringPolicy,但是,如果其RollingPolicy也实现了TriggeringPolicy接口,则只需要显式指定前者。-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
<!-- 文件名:logs/project_info.2017-12-05.0.log, 后缀直接改为gz进行压缩-->
<!-- 注意:SizeAndTimeBasedRollingPolicy中 %i和%d令牌都是强制性的,必须存在,要不会报错 -->
<fileNamePattern>logs/app_info.%d.%i.gz</fileNamePattern>
<!-- 每产生一个日志文件,该日志文件的保存期限为30天, ps:maxHistory的单位是根据fileNamePattern中的翻转策略自动推算出来的,例如上面选用了yyyy-MM-dd,则单位为天
如果上面选用了yyyy-MM,则单位为月,另外上面的单位默认为yyyy-MM-dd-->
<maxHistory>30</maxHistory>
<!-- 每个日志文件到10mb的时候开始切分,最多保留30天,但最大到20GB,哪怕没到30天也要删除多余的日志 -->
<totalSizeCap>5MB</totalSizeCap>
<!-- maxFileSize:这是活动文件的大小,默认值是10MB,测试时可改成5KB看效果 -->
<maxFileSize>50KB</maxFileSize>
</rollingPolicy>
<!--编码器-->
<encoder>
<!-- pattern节点,用来设置日志的输入格式 ps:日志文件中没有设置颜色,否则颜色部分会有ESC[0:39em等乱码-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
<!-- 记录日志的编码:此处设置字符集 - -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- error 日志-->
<!-- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<!-- 以下的大概意思是:1.先按日期存日志,日期变了,将前一天的日志文件名重命名为XXX%日期%索引,新的日志仍然是project_error.log -->
<!-- 2.如果日期没有发生变化,但是当前日志的文件大小超过10MB时,对当前日志进行分割 重命名-->
<appender name="error_log" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志文件路径和名称-->
<File>logs/app_error.log</File>
<!--是否追加到文件末尾,默认为true-->
<append>true</append>
<!-- ThresholdFilter过滤低于指定阈值的事件。 对于等于或高于阈值的事件,ThresholdFilter将在调用其decision()方法时响应NEUTRAL。 但是,将拒绝级别低于阈值的事件 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level><!-- 低于ERROR级别的日志(debug,info)将被拒绝,等于或者高于ERROR的级别将相应NEUTRAL -->
</filter>
<!--有两个与RollingFileAppender交互的重要子组件。 第一个RollingFileAppender子组件,即RollingPolicy:负责执行翻转所需的操作。
RollingFileAppender的第二个子组件,即TriggeringPolicy:将确定是否以及何时发生翻转。 因此,RollingPolicy负责什么和TriggeringPolicy负责什么时候.
作为任何用途,RollingFileAppender必须同时设置RollingPolicy和TriggeringPolicy,但是,如果其RollingPolicy也实现了TriggeringPolicy接口,则只需要显式指定前者。-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
<!-- 文件名:logs/project_error.2017-12-05.0.log, 后缀直接改为gz进行压缩 -->
<!-- 注意:SizeAndTimeBasedRollingPolicy中 %i和%d令牌都是强制性的,必须存在,要不会报错 -->
<fileNamePattern>logs/app_error.%d.%i.gz</fileNamePattern>
<!-- 每产生一个日志文件,该日志文件的保存期限为30天, ps:maxHistory的单位是根据fileNamePattern中的翻转策略自动推算出来的,例如上面选用了yyyy-MM-dd,则单位为天
如果上面选用了yyyy-MM,则单位为月,另外上面的单位默认为yyyy-MM-dd-->
<maxHistory>30</maxHistory>
<!-- 每个日志文件到10mb的时候开始切分,最多保留30天,但最大到20GB,哪怕没到30天也要删除多余的日志 -->
<totalSizeCap>5MB</totalSizeCap>
<!-- maxFileSize:这是活动文件的大小,默认值是10MB,测试时可改成5KB看效果 -->
<maxFileSize>50KB</maxFileSize>
</rollingPolicy>
<!--编码器-->
<encoder>
<!-- pattern节点,用来设置日志的输入格式 ps:日志文件中没有设置颜色,否则颜色部分会有ESC[0:39em等乱码-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%15.15(%thread)] %-40.40(%logger{40}) : %msg%n</pattern>
<!-- 记录日志的编码:此处设置字符集 - -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!--给定记录器的每个启用的日志记录请求都将转发到该记录器中的所有appender以及层次结构中较高的appender(不用在意level值)。
换句话说,appender是从记录器层次结构中附加地继承的。
例如,如果将控制台appender添加到根记录器,则所有启用的日志记录请求将至少在控制台上打印。
如果另外将文件追加器添加到记录器(例如L),则对L和L'子项启用的记录请求将打印在文件和控制台上。
通过将记录器的additivity标志设置为false,可以覆盖此默认行为,以便不再添加appender累积-->
<!-- configuration中最多允许一个root,别的logger如果没有设置级别则从父级别root继承 -->
<root level="INFO">
<!--如果不想输出到控制台直接注释接口,如果需要输出输出到文件,配置名为info_log或者error_log的appender即可-->
<appender-ref ref="STDOUT"/>
<!--<appender-ref ref="info_log" />-->
<!--<appender-ref ref="error_log" />-->
</root>
<!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 -->
<!-- 级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
<logger name="org.springframework.web" level="INFO">
<appender-ref ref="info_log"/>
<appender-ref ref="error_log"/>
</logger>
<!-- 利用logback输入mybatis的sql日志,
注意:如果不加 additivity="false" 则此logger会将输出转发到自身以及祖先的logger中,就会出现日志文件中sql重复打印-->
<logger name="com.harvey.controller" level="DEBUG" additivity="false">
<appender-ref ref="info_log"/>
<appender-ref ref="error_log"/>
</logger>
<!-- additivity=false代表禁止默认累计的行为,即com.atomikos中的日志只会记录到日志文件中,不会输出层次级别更高的任何appender-->
<logger name="com.atomikos" level="INFO" additivity="false">
<appender-ref ref="info_log"/>
<appender-ref ref="error_log"/>
</logger>
</configuration>
代码
Application
需要定义一个Application类作为项目的启动入口,如下:
@MapperScan("com.harvey.mapper") //扫描mybatis的接口mapper
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
common公共代码
BizException自定义异常
public class BizException extends RuntimeException {
private String msg;
private int code = 500;
public BizException(String msg) {
super(msg);
this.msg = msg;
}
public BizException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public BizException(CodeMsg errorCode) {
super(errorCode.getMsg());
this.msg = errorCode.getMsg();
this.code = errorCode.getCode();
}
public BizException(CodeMsg errorCode, String errorMsg) {
super(errorMsg);
this.msg = errorMsg;
this.code = errorCode.getCode();
}
public BizException(String msg, int code) {
super(msg);
this.msg = msg;
this.code = code;
}
public BizException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
CodeMsg状态码
/**
* 操作提示类
*/
@Data
public class CodeMsg {
/******************** 通用错误码 ********************/
public static CodeMsg SUCCESS = new CodeMsg(true, 10000, "success");
public static CodeMsg SERVER_ERROR = new CodeMsg(false, 10001, "系统服务异常");
public static CodeMsg BIND_ERROR = new CodeMsg(false, 10002, "参数校验异常");
public static CodeMsg REQUEST_ILLEGAL = new CodeMsg(false, 10003, "请求非法");
public static CodeMsg ACCESS_LIMIT_REACHED = new CodeMsg(false, 10004, "访问太频繁");
public static CodeMsg PARAMETER_ERROR = new CodeMsg(false, 10005, "参数异常,%");
public static CodeMsg REMOTE_REQUEST_ERROR = new CodeMsg(false, 10006, "远程调用失败");
public static CodeMsg UNKNOWN_ERROR = new CodeMsg(false, 99998, "未知异常");
public static CodeMsg SEARCH_DATA_EMPTY = new CodeMsg(false, 99999, "暂未查到相关信息");
/**
* 是否成功, true或false
*/
private boolean success;
/**
* 操作编码
*/
private int code;
/**
* 操作提示
*/
private String msg;
public CodeMsg(boolean success, int code, String msg) {
this.success = success;
this.code = code;
this.msg = msg;
}
public CodeMsg fillArgs(Object... args) {
int code = this.code;
String msg = String.format(this.msg, args);
return new CodeMsg(false, code, msg);
}
}
分页信息类
/**
* 分页工具类
*/
public class PageInfo<T> implements Serializable {
private static final long serialVersionUID = 1800935089461387955L;
/**
* 总记录数
*/
private Integer total;
/**
* 每页记录数
*/
private Integer pageSize;
/**
* 当前页数
*/
private Integer pageNo;
/**
* 列表数据
*/
private List<T> records;
/**
* 分页
*
* @param records 当前页列表数据
* @param totalCount 总记录数
* @param pageSize 每页记录数
* @param currPage 当前页数
*/
public PageInfo(List<T> records, int totalCount, int pageSize, int currPage) {
this.records = records;
this.total = totalCount;
this.pageSize = pageSize;
this.pageNo = currPage;
}
/**
* 分页(主要用于内存分页, 每次都是查询全部数据再分页)
*
* @param records 总的列表数据
* @param pageSize 每页记录数
* @param currPage 当前页数
*/
public PageInfo(List<T> records, int pageSize, int currPage) {
if (records == null) {
records = new ArrayList();
}
this.total = records.size();
this.pageSize = pageSize;
this.pageNo = currPage;
//总记录数和每页显示的记录之间是否可以凑成整数(pages)
boolean full = this.total % pageSize == 0;
int pages = 0;
if (!full) {
pages = this.total / pageSize + 1;
} else {
pages = this.total / pageSize;
}
int fromIndex = 0;
int toIndex = 0;
fromIndex = pageNo * pageSize - pageSize;
if (this.pageNo <= 0) {
//如果查询的页数小于等于0,则展示第一页
this.pageNo = 1;
toIndex = pageSize;
} else if (this.pageNo >= pages) {
this.pageNo = pages;
toIndex = this.total;
} else {
toIndex = this.pageNo * this.pageSize;
}
if (records.size() == 0) {
this.records = records;
} else {
this.records = records.subList(fromIndex, toIndex);
}
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public Integer getTotal() {
return total;
}
public void setTotal(Integer total) {
this.total = total;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
public Integer getPageNo() {
return pageNo;
}
public void setPageNo(Integer pageNo) {
this.pageNo = pageNo;
}
public List<T> getRecords() {
return records;
}
public void setRecords(List<T> records) {
this.records = records;
}
@Override
public String toString() {
return "PageInfo{" +
"total=" + total +
", pageSize=" + pageSize +
", pageNo=" + pageNo +
", records=" + records +
'}';
}
}
返回结果统一类
/**
* 返回结果统一格式
*
* @param <T>
*/
public class ApiResult<T> implements Serializable {
private static final long serialVersionUID = -9085242252623279681L;
/**
* 是否成功
**/
private Boolean success;
/**
* 状态码
**/
private Integer code;
/**
* 消息
**/
private String msg;
/**
* 返回数据
**/
private T data;
public ApiResult(Integer code, String msg, T value) {
this.code = code;
this.msg = msg;
this.data = value;
}
public ApiResult() {
}
private ApiResult(CodeMsg codeMsg) {
if (codeMsg != null) {
this.success = codeMsg.isSuccess();
this.code = codeMsg.getCode();
this.msg = codeMsg.getMsg();
}
}
public static <T> ApiResult<T> success(T t) {
ApiResult<T> result = new ApiResult(CodeMsg.SUCCESS);
result.setData(t);
return result;
}
public static <T> ApiResult<T> success() {
ApiResult<T> result = new ApiResult(CodeMsg.SUCCESS);
return result;
}
public static <T> ApiResult<T> error(CodeMsg codeMsg) {
return new ApiResult<T>(codeMsg);
}
public static <T> ApiResult<T> error(Integer code, String msg) {
if (null == code) {
code = CodeMsg.UNKNOWN_ERROR.getCode();
}
if (null == msg || msg == "") {
msg = CodeMsg.UNKNOWN_ERROR.getMsg();
}
ApiResult<T> result = new ApiResult();
result.setCode(code);
result.setMsg(msg);
return result;
}
public void setSuccess(boolean success) {
this.success = success;
}
public boolean isSuccess() {
return this.code == CodeMsg.SUCCESS.getCode();
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "Result{" +
"success=" + success +
", code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
config配置
全局异常统一类
/**
* 全局异常
**/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 缺失参数异常处理器
*
* @param e 缺失参数异常
* @return ResponseResult
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ApiResult parameterMissingExceptionHandler(MissingServletRequestParameterException e) {
return ApiResult.error(CodeMsg.SERVER_ERROR.getCode(), "请求参数 " + e.getParameterName() + " 不能为空");
}
/**
* 缺少请求体异常处理器
*
* @param e 缺少请求体异常
* @return ResponseResult
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ApiResult parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) {
return ApiResult.error(CodeMsg.SERVER_ERROR.getCode(), "参数体不能为空");
}
/**
* 自定义异常
**/
@ExceptionHandler(value = BizException.class)
public ApiResult handle(BizException e) {
return ApiResult.error(e.getCode(), e.getMsg());
}
/**
* 全局异常
**/
@ExceptionHandler(Exception.class)
public ApiResult exceptionHandler(HttpServletRequest request, Exception exception) throws Exception {
String message = exception.getMessage() + request.getRequestURL().toString();
return ApiResult.error(CodeMsg.SERVER_ERROR.getCode(), message);
}
/**
* 校验异常
**/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ApiResult handleValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
StringBuilder message = new StringBuilder();
if (bindingResult.hasErrors()) {
FieldError fieldError = bindingResult.getFieldError();
if (fieldError != null) {
message.append(fieldError.getDefaultMessage()).append("!");
}
}
return ApiResult.error(CodeMsg.BIND_ERROR.getCode(), message.toString());
}
/**
* 如果是controller方法中单个参数的校验, 则需要捕获该异常
* 如方法参数:@Length(max = 4) @RequestParam("username") String username
* 需要在controller类上添加@Validated,否则不生效
**/
@ExceptionHandler(value = ConstraintViolationException.class)
public ApiResult handleConstraintViolationException(ConstraintViolationException e) {
StringBuilder message = new StringBuilder();
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
constraintViolations.stream().forEach(item -> {
message.append(item.getMessage()).append("!");
});
return ApiResult.error(CodeMsg.BIND_ERROR.getCode(), message.toString());
}
}
Spring MVC配置
@Configuration
public class AppConfig implements WebMvcConfigurer {
/**
* 跨域配置
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 拦截所有的请求
.allowedOrigins("*") // 可跨域的域名,可以为 *
.allowCredentials(true)
.allowedMethods("*") // 允许跨域的方法,可以单独配置
.allowedHeaders("*"); // 允许跨域的请求头,可以单独配置
}
}
mybatis配置
@Configuration
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
//数据库类型, 单一数据库类型建议设置
paginationInnerInterceptor.setDbType(DbType.MYSQL);
//溢出总页数后是否进行处理, 默认false,不处理
paginationInnerInterceptor.setOverflow(true);
//单页分页条数限制, 默认无限制
paginationInnerInterceptor.setMaxLimit(10000L);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
util工具类
map和bean的相互转换
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public final class BeanMapUtilByIntros {
/**
* 对象转Map
*
* @param object
* @return
*/
public static Map beanToMap(Object object) throws Exception {
Map<String, Object> map = new HashMap<String, Object>();
BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass());
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor property : propertyDescriptors) {
String key = property.getName();
if (key.compareToIgnoreCase("class") == 0) {
continue;
}
Method getter = property.getReadMethod();
Object value = getter != null ? getter.invoke(object) : null;
map.put(key, value);
}
return map;
}
/**
* map转对象
*
* @param map
* @param beanClass
* @param <T>
* @return
* @throws Exception
*/
public static <T> T mapToBean(Map map, Class<T> beanClass) throws Exception {
T object = beanClass.newInstance();
BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass());
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor property : propertyDescriptors) {
Method setter = property.getWriteMethod();
if (setter != null) {
setter.invoke(object, map.get(property.getName()));
}
}
return object;
}
}
业务代码
实体类
@Data
@TableName("tb_user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "name")
private String name;
@TableField(value = "age")
private Integer age;
}
mapper接口
@Repository
public interface UserMapper extends BaseMapper<User> {
}
mapper xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.harvey.mapper.UserMapper">
<resultMap type="com.harvey.entity.User" id="userData">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
</resultMap>
</mapper>
业务逻辑类
public interface UserService extends IService<User> {
}
/**
* 服务接口实现类
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
控制器
@Slf4j
@Validated
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
/**
* 查询用户详情
*
* @param userId
* @return
*/
@GetMapping("getUserDetail")
public ApiResult getUserDetail(@NotNull(message = "用户id不能为空") Integer userId) {
return ApiResult.success(userService.getById(userId));
}
/**
* 新增用户
*
* @param user
* @return
*/
@PostMapping("saveUser")
public ApiResult saveUser(@RequestBody User user) {
userService.save(user);
return ApiResult.success();
}
}
直接idea执行Application中的main函数即可。启动之后,在浏览器输入:http://127.0.0.1:9999/user/getUserDetail?userId=1,得到如下结果 :
{"success":true,"code":10000,"msg":"success","data":{"id":1,"name":"王羲之","age":55}}
新建maven archetype
在spring boot 项目的根目录执行
mvn archetype:create-from-project
如果报错:
[ERROR] The specified user settings file does not exist: C:\Users\harvey\.m2\settings.xml
则将maven安装目录下的配置文件复制一份过去就行。
在target下面会生成archetype信息:
参考:https://maven.apache.org/guides/mini/guide-creating-archetypes.html
发布到本地maven库
cd target/generated-sources/archetype
mvn install
基于archetype搭建工程
根据生成archetype新建一个maven工程。
这里需要注意下ArtifacctId比原来项目中多了一个“-archetype”,如下图:
通过IDEA新建如下:
服务demo方式脚手架
就是搭建一个微服务demo,有新构建项目需求时候,直接下载这个代码就可以。目前有很多开源领域脚手架。
- eladmin (8.9k star):权限管理系统。
- renren(约2.1k) :Java项目脚手架
- SpringBlade (2.6k star) :一个由商业级项目升级优化而来的 SpringCloud 分布式微服务架构、SpringBoot 单体式微服务架构并存的综合型项目。
- COLA (2.1k star):创建属于你的干净的面向对象和分层架构项目骨架。
- SpringBoot_v2 :努力打造springboot框架的极致细腻的脚手架。
利用脚手架生成 新项目 命令行方式
mvn archetype:generate \
-DarchetypeGroupId=com.xxx \
-DarchetypeArtifactId=archetype-spring-boot \
-DarchetypeVersion=1.0.0 \
-DgroupId=com.xxx \
-DartifactId=demo-archetype-generate \
-Dversion=1.0.0 \
-DarchetypeCatalog=internal \
-X
命令说明:
- -DarchetypeGroupId=com.xxx 脚手架的groupId
- -DarchetypeArtifactId=archetype-spring-boot 脚手架的artifactId
- -DarchetypeVersion=1.0.0 脚手架版本
- -DgroupId=com.xxx 需要生成的项目groupId
- -DartifactId=demo-archetype-generate 需要生成的项目artifactId
- -Dversion=1.0.0 需要生成的版本号
- -DarchetypeCatalog=internal 使用私有仓库脚手架jar包, 前提:已经把脚手架发布到私有仓库中
- -DarchetypeCatalog=local 使用本地脚手架jar包, 如果不配置, 它会到中央仓库去下载, 会导致失败
- -X debug模式
使用方式:
使用本地仓库需要clone本项目, install一次, 再到需要生成项目的目录下去执行命令。
使用私有仓库脚手架,不需要clone项目,只需要配置好maven,settting.xml,并执行以上命令即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
2022-03-12 基于XML的入门示例
2022-03-12 JDBC
2022-03-12 递归
2022-03-12 链表
2022-03-12 队列
2022-03-12 栈
2022-03-12 冒泡、选择、插入排序算法