Spring脚手架创建简记
# 前言
不同的企业开发新项目时都会用相同的框架,经过长期的迭代和扩展,每个团队可能都会有固定的框架模板,这里笔者就来介绍一下自己常用的一套脚手架的制作过程。
# 初始化项目创建
# 配置spring-boot基本参数信息
我们首先要到spring官网初始化一个项目出来,地址为:https://start.spring.io/ (opens new window),然后按照界面选项找到自己需要的配置进行修改,如下图所示:

# 添加web依赖
因为我们的spring boot是用于web开发的,所以我们自然是需要引入web依赖,所以我们点击下图的所示按钮,添加web依赖。

然后找到web的依赖,点击添加

完成后界面如下图所示:

# 点击下载
此时我们的基本项目初始化都完成了,我们点击下载。

此时我们的就会得到这样一个文件,我们将其解压后就是一个maven项目,我们将其导入IDEA。

最终我们就会得到这样一个maven项目,项目结构如下图所示:

# 修改版本号
因为spring boot官网界面不提供老版本的生成,考虑到团队规范还有兼容性的原因,我们希望将spring boot版本调整为2.4.0,所以我们这里需要修改一下版本号。

自此我们的项目初始化工作完成了。
# 项目初始化配置
# 统一编码格式
为了保证团队编码统一,我们会要求所有人的IDEA编码都设置为UTF-8。我们打开设置找到文件编码,全部设置为UTF-8

同理ssh终端这边也顺手设置为UTF-8

# 配置JDK和maven
我们初始化时指定项目版本为JDK8,所以我们这里也需要配置一下IDEA的jdk为jdk8版本。我们点击setting找到project structure,将jdk设置为jdk8。

同理在项目设置中指定我们本地安装好的maven,如下图所示:

# 关联远程git仓库
为了保证后续项目可以由版本控制以便查看到提交记录和问题归档,我们这里需要将项目提交到gitee。
首先我们到gitee点击创建仓库:

创建好项目名称,直接点击生成。

如下图所示,我们在gitee官网创建了一个名为web-template的私有仓库之后,会输出下面这些命令,接下来各位可以注意笔者操作确保我们完成项目初始化git。

首先在idea的终端中输入这段命令
git init
如下图所示,输入初始化命令,可以看到笔者IDEA终端为bash而不是cmd,感兴趣的读者可以参考笔者这篇文章:IDEA中Terminal配置为bash (opens new window)

然后我们就可以看到IDEA中出现git的操作log了,我们后续就可以基于这些控件提交代码信息。

然后键入下面这段命令,初始化一个readme文件
touch README.md
然后我们修改一下.gitignore,内容如下所示,读者直接cv即可。
/log/
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
然后我们就直接关联远程仓库,从gitee中复制这条命令
git remote add origin https://gitee.com/xxxxx/web-template.git
2
然后点击commit准备提交代码。

选择要提交的文件,点击push

最后点击push提交,自此git配置全部完成。

回到gitee即可看到我们的修改。

自此我们的项目基本配置就算完成了,后面我们就需要开始对项目中各个功能点进行配置优化了。
# 项目编码配置
# 启动日志优化
我们后续开发时肯定会用到日志,我们这里我们需要对日志格式进行配置一下,首先读者可以自行创建一个名为logback-spring.xml的文件到resources目录下,然后添加如下内容,所有内容笔者都已经注释,读者可以自行查看调整:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 修改一下路径-->
<property name="PATH" value="./log"></property>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-50logger{50}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>-->
<Pattern>%d{ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>
</encoder>
</appender>
<appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/trace.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/trace.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<root level="ERROR">
<appender-ref ref="ERROR_FILE" />
</root>
<root level="TRACE">
<appender-ref ref="TRACE_FILE" />
</root>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
完成后项目就如下图所示:

尝试启动一下,可以看到日志的格式修改了。

# 修改启动的模板
我们希望项目启动时,可以看到详细的启动信息,按照笔者习惯启动类会下面这段代码
@SpringBootApplication
public class WebTemplateApplication {
private static final Logger LOG = LoggerFactory.getLogger(WebTemplateApplication.class);
public static void main(String[] args) {
SpringApplication app = new SpringApplication(WebTemplateApplication.class);
Environment env = app.run(args).getEnvironment();
LOG.info("启动成功!!");
LOG.info("地址: \thttp://127.0.0.1:{}", env.getProperty("server.port"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
然后我们到配置文件配置一下端口号

再次启动,可以看到输出如下结果,自此日志配置完成。

# 开发helloWord接口以及项目分层
为了确认我们的项目是否可以使用,我们这里编写一个简单的helloWorld持续,创建一个controller,如下图所示,我们创建一个controller文件夹,添加一个TestController,代码如下:
@RestController
public class TestController {
private static final Logger LOG = LoggerFactory.getLogger(TestController.class);
@GetMapping("/hello")
public String hello() {
return "Hello World!";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
此时项目结构如下图所示:

完成后我们启动测试一下,可以看到输出了正确结果,说明项目基本到目前没有问题。

# 调整启动类
为了保证后续代码的整洁,我们团队建议将启动类放到config文件夹下,所以我们需要创建一个config文件夹,将启动类剪切过去。
如下图所示,可以看到笔者这里顺手把启动类的名字改了。

因为启动类目录变了,我们默认注解扫描不到bean,我们这里手动添加组件扫描注解,完整代码如下
@ComponentScan("com.sharkChili")
@SpringBootApplication
public class WebApplication {
private static final Logger LOG = LoggerFactory.getLogger(WebApplication.class);
public static void main(String[] args) {
SpringApplication app = new SpringApplication(WebApplication.class);
Environment env = app.run(args).getEnvironment();
LOG.info("启动成功!!");
LOG.info("地址: \thttp://127.0.0.1:{}", env.getProperty("server.port"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
然后我们再次启动测试,可以看到hello接口还是没有问题,自此我们的启动类配置也完成了。

# 集成热部署
我们希望自己的项目一修改项目就会自动重新部署启动,所以我们这里需要配置一下热部署。首先我们在maven里面配置一下热部署的依赖
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
2
3
4
5
然后到设置勾选自动编译

然后键盘点击两下shift出现一个弹窗的东西搜索registry,如下图,然后我们点击进去

如下图,然后勾选动态编译选项

我们现在重启项目然后每次修改点击ctrl+s,项目自动重启部署了。
# 数据库初始化
我们日常开发都是需要用到数据库的,所以我们这里还需要进行数据库初始化工作,首先我们需要准备数据库脚本,以笔者为例,创建一个doc文件夹,然后添加自己的建表脚本。

表内容如下,我们将其刷到自己的数据库中:
-- 测试脚本
drop table if exists `test`;
create table `test` (
`id` bigint not null comment 'id',
`name` varchar(50) comment '名称',
`password` varchar(50) comment '密码',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment '测试';
insert into test(id,name,password) values (1,'测试','123');
drop table if exists `demo`;
create table `demo` (
`id` bigint not null comment 'id',
`name` varchar(50) comment '名称',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment '测试';
insert into `demo` (id, name) VALUES (1, "测试");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
自此我们的数据库初始化完成。

# 集成持久层框架mybatis
接下来我们就要集成持久层框架mybatis了,首先我们需要引入这两个依赖,需要注意笔者的mysql为mysql8读者如果为5可能需要自行调整一下。
<!-- 集成mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- 集成mysql连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
然后配置一下数据源信息
# 增加数据库连接
spring.datasource.url=jdbc:mysql://xxxxxxx.mysql.rds.aliyuncs.com/test_db?characterEncoding=UTF8&autoReconnect=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.datasource.username=xxxxx
spring.datasource.password=xxxxxx
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
2
3
4
5
增加一个domain文件夹,存放和mysql表映射的实体类,然后添加一个test对象,代码如下
public class Test {
private Integer id;
private String name;
private String password;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
完成后项目结构如下:

然后就开始持久层的编写了,首先我们需要创建一个mapper文件夹,添加一个TestMapper

代码内容如下
public interface TestMapper {
public List<Test> list();
}
2
3
4
5
同理resources下创建一个mapper文件夹,创建一个TestMapper.xml

添加的内容如下,读者可以直接照抄,注意namespace为TestMapper接口的包路径还有resultType为实体的包路径。
<?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="com.sharkChili.webTemplate.mapper.TestMapper" >
<select id="list" resultType="com.sharkChili.webTemplate.domain.Test">
select `id`, `name`, `password` from `test`
</select>
</mapper>
2
3
4
5
6
7
8
9
10
11
12
13
为了让我们的mapper接口可以被spring扫描到,我们这里需要加一个组件扫描

完整的代码如下所示
@ComponentScan("com.sharkChili")
@MapperScan("com.sharkChili.webTemplate.mapper")
@SpringBootApplication
public class WebApplication {
private static final Logger LOG = LoggerFactory.getLogger(WebApplication.class);
public static void main(String[] args) {
SpringApplication app = new SpringApplication(WebApplication.class);
Environment env = app.run(args).getEnvironment();
LOG.info("启动成功!!");
LOG.info("地址: \thttp://127.0.0.1:{}", env.getProperty("server.port"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
同理xml也要配置,确保xml也会被扫描到,我们在resource的配置文件中添加这段内容
# 配置mybatis所有Mapper.xml所在的路径
mybatis.mapper-locations=classpath:/mapper/**/*.xml
2
自此我们的基本初始化都完成了,我们不妨创建一个service文件夹,编写一个testService

代码如下
@Service
public class TestService {
@Resource
private TestMapper testMapper;
public List<Test> list() {
return testMapper.list();
}
}
2
3
4
5
6
7
8
9
10
11
同理controller添加一个方法
@GetMapping("/test/list")
public List<Test> list() {
return testService.list();
}
2
3
4
然后我们测试一下接口,如果可以正常查询则说明mybaits配置基本配置完成了。
# 集成官方生成器
持久层crud逻辑都差不多,我们希望有个工具可以自动生成,这里我们建议使用mybaits自带的生成器。
首先我们引入mybatis官方的生成器插件和mysql连接插件。
<!-- mybatis generator 自动生成代码插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<configurationFile>src/main/resources/generator/generator-config.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
</dependencies>
</plugin>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后在resources文件夹创建一个generator文件夹,添加generator-config.xml,内容如下,读者按需修改即可
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">
<!-- 自动检查关键字,为关键字增加反引号 -->
<property name="autoDelimitKeywords" value="true"/>
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<!--覆盖生成XML文件-->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
<!-- 生成的实体类添加toString()方法 -->
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<!-- 不生成注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://rm-xxxxxx.mysql.rds.aliyuncs.com:3306/wikidev?serverTimezone=Asia/Shanghai"
userId="xxxx"
password="xxxxxx">
</jdbcConnection>
<!-- domain类的位置 -->
<javaModelGenerator targetProject="src\main\java"
targetPackage="com.sharkChili.webTemplate.domain"/>
<!-- mapper xml的位置 -->
<sqlMapGenerator targetProject="src\main\resources"
targetPackage="mapper"/>
<!-- mapper类的位置 -->
<javaClientGenerator targetProject="src\main\java"
targetPackage="com.sharkChili.webTemplate.mapper"
type="XMLMAPPER"/>
<table tableName="demo" domainObjectName="Demo"/>
</context>
</generatorConfiguration>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
然后添加maven的命令,用于触发mybatis自动生成器,首先点击配置

选择maven配置

然后输入选择我们的项目文件夹,并输入生成命令

执行看看类会不会出来

可以看到类生成了,然后我们就可以编写controller进行测试了,自此mybatis生成器集成完成了。

以笔者为例service代码如下
@Service
public class DemoService {
@Resource
private DemoMapper demoMapper;
public List<Demo> list() {
return demoMapper.selectByExample(null);
}
}
2
3
4
5
6
7
8
9
10
11
controller代码如下
@RestController
@RequestMapping("/demo")
public class DemoController {
@Resource
private DemoService demoService;
@GetMapping("/list")
public List<Demo> list() {
return demoService.list();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
我们发现在调试过程中,控制台没有打印sql日志,如果读者希望调试的时候可以看到sql打印日志,我们可以在配置文件中添加下面这段配置:
# 通用返回值
为了保证和前端联调时响应参数的一致,我们这里需要编写一个通用返回值的类。所以我们需要创建一个resp文件夹,并创建一个CommonResp类,代码如下
package com.sharkChili.webTemplate.resp;
public class CommonResp<T> {
/**
* 业务上的成功或失败
*/
private boolean success = true;
/**
* 返回信息
*/
private String message;
/**
* 返回泛型数据,自定义类型
*/
private T content;
public boolean getSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("ResponseDto{");
sb.append("success=").append(success);
sb.append(", message='").append(message).append('\'');
sb.append(", content=").append(content);
sb.append('}');
return sb.toString();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
我们调整一下控制器的代码

返回值调整一下
@RestController
@RequestMapping("/demo")
public class DemoController {
@Resource
private DemoService demoService;
@GetMapping("/list")
public CommonResp<List<Demo>> list() {
CommonResp<List<Demo>> resp=new CommonResp<>();
resp.setContent(demoService.list();
return resp;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 封装请求参数和返回值
我们的请求参数有时候不一定是和实体类一致,返回值同理,所以我们也需要创建一个请求参数和返回值的文件夹管理这些参数。
我们以demo为例,创建一个req文件夹,复制demo类然后直接修改类名为DemoReq,后续需要加参数再调整。

resp同理者不多赘述。然后我们改一下controller和service的逻辑,先来看看service
@GetMapping("/demo/list")
public List<DemoResp> list(DemoReq req) {
DemoExample demoExample = new DemoExample();
DemoExample.Criteria criteria = demoExample.createCriteria();
if (StrUtil.isNotEmpty(req.getName())){
criteria.andNameLike("%"+req.getName()+"%");
}
List<Demo> demoList = demoMapper.selectByExample(demoExample);
List<DemoResp> resultList=new ArrayList<>();
demoList.forEach(d->{
DemoResp resp=new DemoResp();
BeanUtils.copyProperties(d,resp);
resultList.add(resp);
});
return resultList;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后是controller
@GetMapping("/list")
public CommonResp<List<DemoResp>> list(DemoReq req) {
CommonResp<List<DemoResp>> resp=new CommonResp<>();
resp.setContent(demoService.list(req));
return resp;
}
2
3
4
5
6
我们点击测试一下,没有问题就说明测试完成了。
# bean拷贝工具copyUtil制作
上文我们将demo转为resp对象时,代码非常长且不优雅,我们这里进行优化一下。创建一个util文件夹,编写一个工具copyUtil
public class CopyUtil {
/**
* 单体复制
*/
public static <T> T copy(Object source, Class<T> clazz) {
if (source == null) {
return null;
}
T obj = null;
try {
obj = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
BeanUtils.copyProperties(source, obj);
return obj;
}
/**
* 列表复制
*/
public static <T> List<T> copyList(List source, Class<T> clazz) {
List<T> target = new ArrayList<>();
if (!CollectionUtils.isEmpty(source)){
for (Object c: source) {
T obj = copy(c, clazz);
target.add(obj);
}
}
return target;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
然后我们的代码重构一下
@GetMapping("/demo/list")
public List<DemoResp> list(DemoReq req) {
DemoExample demoExample = new DemoExample();
DemoExample.Criteria criteria = demoExample.createCriteria();
if (StrUtil.isNotEmpty(req.getName())){
criteria.andNameLike("%"+req.getName()+"%");
}
List<Demo> demoList = demoMapper.selectByExample(demoExample);
List<DemoResp> resultList = CopyUtil.copyList(demoList, DemoResp.class);
return resultList;
}
2
3
4
5
6
7
8
9
10
11
12
# 请求拦截
我们希望所有的请求都可以进行监控,所以我们会通过AOP对用户的请求进行拦截,首先我们添加aop和fastJson的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
然后util下增加这两个类
/**
* Twitter的分布式自增ID雪花算法
**/
@Component
public class SnowFlake {
/**
* 起始的时间戳
*/
private final static long START_STMP = 1609459200000L; // 2021-01-01 00:00:00
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 5; //机器标识占用的位数
private final static long DATACENTER_BIT = 5;//数据中心占用的位数
/**
* 每一部分的最大值
*/
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId = 1; //数据中心
private long machineId = 1; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L;//上一次时间戳
public SnowFlake() {
}
public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
/**
* 产生下一个ID
*
* @return
*/
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}
lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
| datacenterId << DATACENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}
private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}
private long getNewstmp() {
return System.currentTimeMillis();
}
public static void main(String[] args) throws ParseException {
// 时间戳
// System.out.println(System.currentTimeMillis());
// System.out.println(new Date().getTime());
//
// String dateTime = "2021-01-01 08:00:00";
// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// System.out.println(sdf.parse(dateTime).getTime());
SnowFlake snowFlake = new SnowFlake(1, 1);
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
System.out.println(snowFlake.nextId());
System.out.println(System.currentTimeMillis() - start);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
第二个类
package com.sharkChili.webTemplate.util;
import java.io.Serializable;
public class RequestContext implements Serializable {
private static ThreadLocal<String> remoteAddr = new ThreadLocal<>();
public static String getRemoteAddr() {
return remoteAddr.get();
}
public static void setRemoteAddr(String remoteAddr) {
RequestContext.remoteAddr.set(remoteAddr);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
然后创建一个aspect文件夹,添加以下内容
@Aspect
@Component
public class LogAspect {
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/** 定义一个切点 */
@Pointcut("execution(public * com.sharkChili.*.controller..*Controller.*(..))")
public void controllerPointcut() {}
@Resource
private SnowFlake snowFlake;
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
// 打印请求信息
LOG.info("------------- 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
RequestContext.setRemoteAddr(getRemoteIp(request));
// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
/**
* 使用nginx做反向代理,需要用该方法才能取到真实的远程IP
* @param request
* @return
*/
public String getRemoteIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
可以看到自此我们的日志监控也完成了。

# 跨域问题解决
我们后续会和前端进行联调开发等,所以前后端代码交互的过程中可能会出现跨域问题。所以我们还需跨域问题。所以我们不妨在config文件夹下创建一个CorsConfig类。键入下面这段内容:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL)
.allowCredentials(true)
.maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
此时代码结构如下图所示:

# 小结
这套脚手架算是笔者用过比较习惯的一套脚手架,读者可以根据读者的配置自行修改调整。笔者会在后续文章对该脚手架进行扩展,实现:
- 校验框架
- 分页插件
- 代码生成器
# 补充:spring配置文件内容转换神奇
有时候我们的查找网上配置时发现网上用的是yaml配置,而我们用的确实properties格式,我们建议使用这个网站做在线转换:https://www.toyaml.com/index.html (opens new window) 可以看到笔者输入properties的配置参数,点击一下即可变成yaml配置。
