# Spring 开发
Spring 框架开发常用知识点。
# 配置文件
# context-path
# 一个访问地址是 http://localhost/index
# 这里配置完之后就成了 http://localhost/test/index
server:
servlet:
context-path: /test
2
3
4
5
# Validation
数据校验。
# JSR 303
# Java Bean Validation
# Spring Validation
# Spring Boot Validation
示例:
# 定时任务
@EnableScheduling
(启动类上) +@Scheduled(cron = "xxxxx")
(方法上)- cron 表达式
- interface: TaskExecutor、TaskScheduler
# Cron 表达式
表达式一共 6 位,依次表示为 秒、分、时、月天、月、周天。
特殊字符:
*
任意,
枚举-
区间/
步长?
日/星期冲突匹配L
最后W
工作日C
和 calendar 联系后计算过的值#
星期,4#2,第 2 个星期四
示例: 0 * * * * MON-FRI
,*
表示任意时刻。
@Scheduled(cron = "0 * * * * 2-4")
- 每周二到周四的 0 秒执行一次,即每周二的每分钟执行一次。
@Scheduled(cron = "0,1,2,3,4 * * * * 2-4")
- 每周二到周四的 0、1、2、3、4 秒都执行一次。
@Scheduled(cron = "0-4 * * * * 2-4")
- 每周二到周四的 0、1、2、3、4 秒都执行一次。
@Scheduled(cron = "0/4 * * * * 2-4")
- 每周二到周四每 4 秒执行一次。
@Scheduled(cron = "0/4 * * ? * 2")
- 每周二到周四每 4 秒执行一次。
- 错误写法:
@Scheduled(cron = "0/4 * * * * 2")
中第三个*
表示任意一天,而最后一个 2 表示只在周二,那么这里就冲突了,接下来就要使用?
解决冲突。
# 分布式任务调度 shedlock
@Scheduled(cron = "1/4 * * * * *")
@SchedulerLock(
name = "cleanTmallScoreTask",
lockAtLeastFor = "5000",
lockAtMostFor = "5m"
)
---
lockAtLeastFor/lockAtMostFor 这两个参数值默认单位是 ms,
也可以使用 5s/5m/5h 这种带单位的组合,也可以使用 PT3M 这种。
2
3
4
5
6
7
8
9
# 异步任务
@EnableAsync + @Async
# 线程池配置
Spring 的异步任务默认使用的线程池是 SimpleAsyncTaskExecutor
,这是个伪线程池,它并不会重用线程。
实际使用中需要配置自定义的线程池使用。
ThreadPoolTaskExecutor
(spring 提供)对 ThreadPoolExecutor
(JDK)做了进一步封装。
示例:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Autowired
private ThreadPoolTaskExecutor threadPool;
@Override
public Executor getAsyncExecutor() {
// 配置默认线程池,也可以使用 @Async("线程池 name") 指定线程池。
// 如果不配置默认线程池,框架默认的线程池为 SimpleAsyncTaskExecutor。这是个伪线程池,不会重用线程。
return threadPool;
}
}
---
@Bean(name = "customExecutor")
public ThreadPoolTaskExecutor asyncServiceExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setMaxPoolSize(20); // max pool size 必须 >= core pool size
threadPoolTaskExecutor.setQueueCapacity(100);
threadPoolTaskExecutor.setKeepAliveSeconds(5);
threadPoolTaskExecutor.setThreadNamePrefix("custom-");
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolTaskExecutor.setTaskDecorator(new MyDecorator()); // 通过装饰器可以增强线程任务
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
---
// 在装饰器中对具体任务做增强,实际开新线程的时候,执行的是这个装饰器返回的 runnable 实例。
public class MyDecorator implements TaskDecorator {
@Override
public Runnable decorate(@NotNull Runnable runnable) {
return () -> {
// 可以在方法前后自定义增强,然后重新封装 runnable。
runnable.run();
};
}
}
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
# CompletableFuture/待整理
CompletableFuture,提供了非常强大的 Future 的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合 CompletableFuture 的方法。 CompletableFuture 类实现了 Future 接口,所以你还是可以像以前一样通过 get 方法阻塞或者轮询的方式获得结果。
# 单任务
runAsync
,无返回值/** * runAsync无返回值 */ CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> { System.out.println("当前线程" + Thread.currentThread().getId()); int i = 10 / 2; System.out.println("运行结果:" + i); });
1
2
3
4
5
6
7
8supplyAsync
,有返回值whenComplete
能感知异常,能感知结果,但没办法给返回值exceptionally
能感知异常,不能感知结果,能给返回值。相当于,如果出现异常就返回这个值handle
能拿到返回结果,也能得到异常信息,也能修改返回值
/** * supplyAsync 有返回值 * whenComplete 能感知异常,能感知结果,但没办法给返回值 * exceptionally 能感知异常,不能感知结果,能给返回值。相当于,如果出现异常就返回这个值 */ CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("当前线程" + Thread.currentThread().getId()); int i = 10 / 0; System.out.println("运行结果:" + i); return i; }).whenComplete((res, exception) -> { // whenComplete 虽然能得到异常信息,但是没办法修改返回值 System.out.println("异步任务成功完成...结果是:" + res + ";异常是:" + exception); }).exceptionally(throwable -> { // exceptionally 能感知异常,而且能返回一个默认值,相当于,如果出现异常就返回这个值 return 10; }); System.out.println(future.get());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/** * supplyAsync 有返回值 * handle 能拿到返回结果,也能得到异常信息,也能修改返回值 */ CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("当前线程" + Thread.currentThread().getId()); int i = 10 / 0; System.out.println("运行结果:" + i); return i; }).handle((res, exception) -> { if (exception != null) { return 0; } else { return res * 2; } }); System.out.println(future.get());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 多任务
thenRunAsync
,不能接收上一次的执行结果,也没返回值/** * thenRunXXX 不能接收上一次的执行结果,也没返回值 * .thenRunAsync(() -> { * System.out.println("任务2启动了..."); * }); */ CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> { System.out.println("当前线程" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("运行结果:" + i); return i; }).thenRunAsync(() -> { System.out.println("任务2启动了..."); }).thenRunAsync(() -> { System.out.println("任务3启动了..."); }); future.get();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17thenAcceptAsync
,能接收上一次的执行结果,但没返回值/** * thenAcceptXXX 能接收上一次的执行结果,但没返回值 * .thenAcceptAsync(res -> { * System.out.println("任务2启动了..."+res); * }); */ CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> { System.out.println("当前线程" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("运行结果:" + i); return i; }).thenAcceptAsync(res -> { System.out.println("任务2启动了..." + res); }); future.get();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15thenApplyAsync
,能接收上一次的执行结果,又可以有返回值/** * thenApplyXXX 能接收上一次的执行结果,又可以有返回值 * .thenApplyAsync(res -> { * System.out.println("任务2启动了..." + res); * return "hello " + res; * }); */ CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> { System.out.println("当前线程" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("运行结果:" + i); return i; }).thenAcceptAsync(res -> { System.out.println("任务2启动了..." + res); }); future.get();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 三任务编排
// 先准备两个任务
CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务1线程" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("任务1结束:");
return i;
});
CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务2线程" + Thread.currentThread().getId());
try {
Thread.sleep(3000);
System.out.println("任务2结束:");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello";
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 三任务组合,前两个任务都完成,才执行任务3
runAfterBothAsync
,任务01、任务02都完成了,再开始执行任务3,不感知任务1、2的结果的,也没返回值CompletableFuture<Void> future = future01.runAfterBothAsync(future02, () -> { System.out.println("任务3开始"); });
1
2
3thenAcceptBothAsync
,任务01、任务02都完成了,再开始执行任务3,能感知到任务1、2的结果,但没返回值CompletableFuture<Void> future = future01.thenAcceptBothAsync(future02, (f1, f2) -> { System.out.println("任务3开始...得到之前的结果:f1:" + f1 + ", f2:" + f2); });
1
2
3thenCombineAsync
,任务01、任务02都完成了,再开始执行任务3,能感知到任务1、2的结果,而且自己可以带返回值CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> { return f1 + ":" + f2 + ":哈哈"; });
1
2
3
# 三任务组合,前两个任务只要有一个完成,就执行任务3
runAfterEitherAsync
,两个任务只要有一个完成,就执行任务3,不感知结果,自己没返回值CompletableFuture<Void> future = future01.runAfterEitherAsync(future02, () -> { System.out.println("任务3开始..."); });
1
2
3acceptEitherAsync
,两个任务只要有一个完成,就执行任务3,感知结果,自己没返回值CompletableFuture<Void> future = future01.acceptEitherAsync(future02, (res) -> { System.out.println("任务3开始...之前的结果" + res); });
1
2
3applyToEitherAsync
,两个任务只要有一个完成,就执行任务3,感知结果,自己有返回值CompletableFuture<String> future = future01.applyToEitherAsync(future02, (res) -> { System.out.println("任务3开始...之前的结果" + res); return "任务3的结果..."; }); System.out.println(future.get());
1
2
3
4
5
# 多任务的编排
/**
* 多任务组合
*/
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品图片信息");
return "hello.jpg";
});
CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
System.out.println("查询商品属性信息");
return "黑色+256G";
});
CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
System.out.println("查询商品介绍信息");
} catch (InterruptedException e) {
e.printStackTrace();
}
return "华为...";
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
allOf
,所有任务都执行完/** * allOf 所有任务都执行完 */ CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc); allOf.get(); // 等待所有结果完成
1
2
3
4
5anyOf
,其中有一个任务执行完就可以/** * anyOf 其中有一个任务执行完就可以 */ CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc); anyOf.get();
1
2
3
4
5
# Filter 过滤器
过滤器的使用
# 实现方式
- implements Filter (javax.servlet) +
@Component
- 默认拦截所有路径。
- implements Filter (javax.servlet) +
@WebFilter(filterName = "xxx", urlPatterns = {"/*"})
+@ServletComponentScan
(启动类上)
# 流只能读一次的原因
使用 Spring Boot 框架来做 web 开发的核心是 DispatcherServlet,也就是使用 Servlet 来进行网络请求的处理,会从 HttpServletRequest 中获取请求参数等信息。通过HttpServletRequest获取流的代码片:
try {
ServletInputStream stream = request.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
2
3
4
5
可知会返回一个 ServletInputStream,类继承关系为:
public abstract class ServletInputStream extends InputStream {...}
public abstract class InputStream implements Closeable {...}
public interface Closeable extends AutoCloseable {...}
2
3
4
5
查看 InputStream 的源码可知,读取流的时候会根据 position 来获取当前位置,并且随着读取来进行位置的移动。如果想要重新读取,可以调用 inputstream.reset 方法,但是能否 reset 取决于 markSupported 方法,返回 true 可以 reset,反之不行。查看 ServletInputStream 可知,这个类并没有复写 markSupported 和 reset 方法,查看父类 InputStream:
public boolean markSupported() {
return false;
}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
2
3
4
5
6
可知 ServletInputStream 不支持 reset,故这个流只能读取一次。
# 过滤器实例
要实现:获取 request body 、response body,计算 response time。
难点:
- 直接在 HttpServletRequest 中无法获取 request body,另外 HttpServletRequest 的输入流只能读取一次,在达到 Controller 之前如果读取了这个流,那么在 Controller 中就会获取失败。
- 拦截器中无法通过包装类获取到 response body,只能在 filter 中去做。
实现:
- 编写 RequestWrapper,将输入流永久保存在对象中(一个属性中),后面通过传递这个对象来实现 body 的无限次读取。
- 类中需要重写
getInputStream()
、getReader()
这两个方法,因为在后续的操作中框架会使用到这两个方法。 否则的话在后续的操作中,比如在 Controller 中,就会无法获取 request body。
- 类中需要重写
- filter 中
doFilter()
中的应该传递包装类chain.doFilter(requestWrapper, responseWrapper);
点击查看示例代码
@Data
public class MyLog {
private Object body;
private int responseTime;
private int responseBodySize;
}
---
@Component
public class XxxFilter implements Filter {
private static final ThreadLocal<MyLog> MY_LOG = new ThreadLocal<>();
// 因为 Filter 没有 excludeURL 这种处理,所以需要自定义一个数组来实现。
// 适用于只排除个别路径的情况
private static final Set<String> EXCLUDE_PATHS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("/ping")));
@Autowired
private ObjectMapper objectMapper;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) request);
ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) response);
String requestURI = requestWrapper.getRequestURI();
// 如果是需要忽略的,则不做任何处理,直接向下传递
if(EXCLUDE_PATHS.contains(requestURI)) {
chain.doFilter(requestWrapper, responseWrapper);
} else {
// 应该被拦截的路径,然后自定义处理 ...
MyLog myLog = new MyLog();
byte[] bodyBytes = requestWrapper.getBodyBytes();
Object body = objectMapper.readValue(new String(bodyBytes, "utf-8"), Object.class);
myLog.setBody(body);
long startTime = System.currentTimeMillis();
chain.doFilter(requestWrapper, responseWrapper);
long responseTime = System.currentTimeMillis() - startTime;
myLog.setResponseTime(responseTime);
myLog.setResponseBodySize(responseWrapper.getBodySize());
MY_LOG.set(myLog);
System.out.println(objectMapper.writeValueAsString(MY_LOG.get()));
MY_LOG.remove();
}
}
}
---
public class RequestWrapper extends HttpServletRequestWrapper {
private byte[] bodyBytes = null;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
}
public byte[] getBodyBytes() {
return bodyBytes;
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyBytes);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
---
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
private HttpServletResponse response;
public ResponseWrapper(HttpServletResponse response) {
super(response);
this.response = response;
}
public byte[] getBody() {
return byteArrayOutputStream.toByteArray();
}
public int getBodySize() {
return getBody().length;
}
@Override
public ServletOutputStream getOutputStream() {
return new ServletOutputStreamWrapper(this.byteArrayOutputStream , this.response);
}
@Override
public PrintWriter getWriter() throws IOException {
return new PrintWriter(new OutputStreamWriter(this.byteArrayOutputStream , this.response.getCharacterEncoding()));
}
@Data
@AllArgsConstructor
private static class ServletOutputStreamWrapper extends ServletOutputStream {
private ByteArrayOutputStream outputStream;
private HttpServletResponse response;
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener listener) {
}
@Override
public void write(int b) throws IOException {
this.outputStream.write(b);
}
@Override
public void flush() throws IOException {
if (! this.response.isCommitted()) {
byte[] body = this.outputStream.toByteArray();
ServletOutputStream outputStream = this.response.getOutputStream();
outputStream.write(body);
outputStream.flush();
}
}
}
}
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# Interceptor 拦截器
拦截器的使用
Interceptor 的执行顺序大致为:
- 请求到达 DispatcherServlet。
- DispatcherServlet 发送至 Interceptor ,执行 preHandle。
- 请求达到 Controller。
- 请求结束后,postHandle 执行。
Spring 中主要通过 HandlerInterceptor
接口来实现请求的拦截,实现 HandlerInterceptor
接口需要实现下面三个方法:
preHandle()
– 在 handler 执行之前,返回 boolean 值,true 表示继续执行,false 为停止执行并返回。postHandle()
– 在 handler 执行之后, 可以在返回之前对返回的结果进行修改。afterCompletion()
– 在请求完全结束后调用,可以用来统计请求耗时等等,无论 Controller 方法是否抛异常都会执行。
常用拦截器有以下三种:
HandlerInterceptor
,MVC 拦截器org.springframework.web.servlet.HandlerInterceptor
ClientHttpRequestInterceptor
,RestTemplate 拦截器org.springframework.http.client.ClientHttpRequestInterceptor
RequestInterceptor
,Feign 拦截器feign.RequestInterceptor
包路径:
# HandlerInterceptor
常规拦截器
- 一般用于拦截客户端浏览器的 http 请求
org.springframework.web.servlet.HandlerInterceptor
# 多个拦截器的执行顺序
拦截器的拦截顺序,是按照 Web 配置文件中注入拦截器的顺序执行的。
即 WebMvcConfig 类中 addInterceptor 的顺序。
# ClientHttpRequestInterceptor
RestTemplate 拦截器
- 用于 RestTemplate 的请求进行拦截的
org.springframework.http.client.ClientHttpRequestInterceptor
# RequestInterceptor
// TODO
# 拦截器 & 过滤器
request -> filter -> servlet -> interceptor -> controller advice -> aspect -> controller
# 多模块中的执行顺序
- 拦截器,Interceptor: 默认的 order 是 0。
@Order 注解不起作用,order 应该在 webMvcConfig 中配置:
@Configuration public class PlayMvcConfig implements WebMvcConfigurer { @Autowired private A1Interceptor a1Interceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(a1Interceptor).order(1); } }
1
2
3
4
5
6
7
8
9
10
过滤器,Filter: 默认的 order 是 int 的最大值。
- 也可以使用 @Order
实现加载顺序。
同等优先级 -> 就近原则 (同等优先级先执行本模块,再执行 core模块,即: 路径最短原则)
外层先执行,core 后执行
# ControllerAdvice
# ResponseBodyAdvice
// TODO
# GlobalExceptionHandler 全局异常处理
多个模块中,就近原则。
如果 A 依赖 core,A 和 core 中都做了全局异常处理,如果在 A 中能处理的就不会执行 core 中的异常处理。
# Embedded Web Server
Spring boot 使用的三大主流 web server:
- Tomcat(Spring boot 内置)
- Jetty(适合长连接应用,就是聊天类的长连接)
- Undertow(不支持jsp)
# 结论先行
- 吞吐量 :Undertow > Jetty > Tomcat
- 响应时间 :Jetty < Tomcat < Undertow
- CPU 使用率:Undertow < Jetty < Tomcat
- 内存使用率:Undertow < Jetty < Tomcat
- 线程数 :Undertow < Jetty < Tomcat
综合性能 Undertow 最强。
# Tomcat
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2
3
4
默认内置 web server 就是 Tomcat。
# Jetty
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
# Undertow
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
# Jackson
JSON 工具类。
# ObjectMapper
# readValue
作用:反序列化,从一个 String 类型的内容转换成指定的类。
示例:将一个 byte[] 转成 Map 类型
ObjectMapper objectMapper = new ObjectMapper(); byte[] bytes; String bytesToString = New String(bytes, "utf-8"); Map map = objectMapper.readValue(bytesToString, Map.class);
1
2
3
4
# convertValue
作用:反序列化,从一个 引用类型(非 String)转换成指定的类。
示例:将一个 byte[] 转成 Map 类型
ObjectMapper objectMapper = new ObjectMapper(); byte[] bytes; String bytesToString = New String(bytes, "utf-8"); Map map = objectMapper.readValue(bytesToString, Map.class);
1
2
3
4
# Jackson 配置
序列化的时候忽略未知属性
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
# JsonProperty
@JsonProperty(value="Key")
private String key;
2
如果一个接口的返回值是 Key 而不是 key,这时使用 JsonProperty 注解可以做到自定义 key 接收来自 Key 的值的时候,将自定义的 key 序列化成 Key,这样自定义的 key 在实际赋值的时候,他就被序列化成大写的 Key,就可以成功接收那个值了。
# AliasFor
- 包路径:
org.springframework.core.annotation.AliasFor
- 表示添加了这个注解的两个属性互为别名
- 互为别名的两个属性都要有默认值,并且默认值应该一致
- 互为别名的两个属性都要添加这个注解,并且标注对方的名称
- 使用注解时,不可以同时给这两个属性赋值,不然在启动的时候会报错
点击查看示例代码
// 两个互为别名的属性,默认值应该一致,否则启动时报错
// value 上面添加了注解 @AliasFor("name"),那么在 name 的属性上面也要添加这个注解声明
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
@AliasFor("name")
String value() default "122";
@AliasFor("value")
String name() default "122";
}
---
// 错误使用,这时,程序启动时会报错
/**
Different @AliasFor mirror values for annotation [com.example.demo.annotation.Encrypt] declared on com.example.demo.annotation.UserController.getUser(); attribute 'name' and its alias 'value' are declared with values of [dfd] and [222].
*/
@Encrypt(value = "222", name = "dfd")
@GetMapping("/get")
public String getUser() {
System.out.println("get 方法 。。。");
return null;
}
---
// 正确使用方式
@Encrypt(value = "222") // 或者 @Encrypt(name = "222")
@GetMapping("/get")
public String getUser() {
System.out.println("get 方法 。。。");
return null;
}
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
- 继承父注解的属性,使其拥有更强大的功能:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
@MyAnnotation
public @interface SubMyAnnotation {
@AliasFor(value="location",annotation=MyAnnotation.class)
String subLocation() default "";
@AliasFor(annotation=MyAnnotation.class) //缺省指明继承的父注解的中的属性名称,则默认继承父注解中同名的属性名
String value() default "";
}
2
3
4
5
6
7
8
9
10
11
12
# JPA
# 方法命名参考
注意:JPA 方法命名规则优先级低于 @Query
注解。
# RestTemplate
Http 相关工具类。
通过 RestTemplate 调用 API,只有返回值 code = 200 的时候才会向下进行,如果是其他的状态码,则直接抛出异常,而不能通过 ResponseEntity 的 HttpStatus 进行判断,因为非 OK(200)的情况直接抛异常(即 exchange()、post()、get() 等方法直接执行失败)。见配置自定义 ResponseErrorHandler。
# 配置
- 配置 MessageConverter 添加自定义 MediaType。
// 1 直接使用 RestTemplate 对象配置
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(
new MediaType("text", "json", StandardCharsets.UTF_8)
));
restTemplate.setMessageConverters(Arrays.asList(mappingJackson2HttpMessageConverter));
// 2 使用 RestTemplateBuilder 配置
RestTemplateBuilder builder = new RestTemplateBuilder();
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(
new MediaType("text", "json", StandardCharsets.UTF_8)
));
builder = builder.additionalMessageConverters(mappingJackson2HttpMessageConverter);
RestTemplate restTemplate = builder.build();
2
3
4
5
6
7
8
9
10
11
12
13
14
- 配置默认编码规则。
// 此处以设置为默认不进行编码为例
RestTemplateBuilder builder = new RestTemplateBuilder();
DefaultUriBuilderFactory defaultUriBuilderFactory = new DefaultUriBuilderFactory();
defaultUriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
builder = builder.uriTemplateHandler(defaultUriBuilderFactory);
RestTemplate restTemplate = builder.build();
2
3
4
5
6
- 配置自定义 ResponseErrorHandler。
RestTempalte
默认的ResponseErrorHandler
是DefaultResponseErrorHandler
,默认处理器会校验 Response 的响应码HttpStautsCode
如果Code = 4xx or 5xx
,就会抛出RestClientException
。如果我们想要根据每种HttpStautsCode
做处理,那就需要让RestTemplate
返回所有的状态码,不要报错。
// 第一步,定义一个 Handler
public class OmsResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
}
// 第二步,配置
restTemplateBuilder.errorHandler(new OmsResponseErrorHandler()).build();
2
3
4
5
6
7
8
9
10
11
12
13
14
- 配置 GET 请求可以携带 body。
GET 请求支持通过 Body 携带参数(HTTP 1.1 开始支持),但是默认的RestTemplate 是不支持的,因为RestTemplate 默认使用的RequestFactory 是SimpleClientHttpRequestFactory ,不支持 GET 传递 Body。 可以自定义配置 RequestFactory ,这里修改为 HTTPClient(使用HttpComponentsClientHttpRequestFactory),默认的HTTPClient 的 GET 请求也不支持携带 Body,这里要重新实现 GET 请求。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
---
public class HttpComponentsClientRestfulHttpRequestFactory extends HttpComponentsClientHttpRequestFactory {
@Override
protected HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
if (httpMethod == HttpMethod.GET) {
return new HttpGetRequestWithEntity(uri);
}
return super.createHttpUriRequest(httpMethod, uri);
}
private static final class HttpGetRequestWithEntity extends HttpEntityEnclosingRequestBase {
public HttpGetRequestWithEntity(final URI uri) {
super.setURI(uri);
}
@Override
public String getMethod() {
return HttpMethod.GET.name();
}
}
}
---
@Bean
public RestTemplate caudalieRestTemplate(RestTemplateBuilder builder) {
return builder.rootUri("xxx")
.requestFactory(HttpComponentsClientRestfulHttpRequestFactory.class)
.build();
}
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
# RedisTemplate
操作 Redis 的一个类。
该类提供了 redis command 对应的方法,直接调用该类对象的方法就可以实现在代码中操作 redis 了。
# 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2
3
4
# MongoTemplate
# 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
2
3
4
# ElasticsearchRestTemplate
# 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2
3
4
# SFTP 服务器
JSch,即 Java Secure Channel。
它是一个 SSH2 的纯 Java 实现,它允许你连接到一个 SSH 服务器,并且可以使用端口转发,X11 转发,文件传输等。
这里写的只是它的 SFTP 功能。
# ChannelSftp 核心类
ChannelSftp 是 JSch 实现 sftp 的核心类,它包含了所有的 SFTP 方法:
put()
文件上传get()
文件下载cd()
进入指定目录ls()
得到指定目录下的文件列表rename()
重命名指定文件或目录rm()
删除指定文件mkdir()
创建目录rmdir()
删除目录pwd()
查看当前所在路径- 等等
# 文件传输模式
JSch 支持三种文件传输模式:
OVERWRITE
完全覆盖模式,这是 JSch 的默认文件传输模式,即如果目标文件已经存在,传输的文件将完全覆盖目标文件,产生新的文件。
RESUME
恢复模式,如果文件已经传输一部分,这时由于网络或其他任何原因导致文件传输中断,如果下一次传输相同的文件, 则会从上一次中断的地方续传。
APPEND
追加模式,如果目标文件已存在,传输的文件将在目标文件后追加。
# 实现
需要编写一个工具类,根据 ip、username 等信息,得到一个 SFTP channel 对象(ChannelSftp 的示例对象), 然后就可以在程序中使用该对象在代码中使用各种方法操作了。
点击查看示例代码
import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.Properties;
@Slf4j
public class SftpUtil {
private Session session = null;
private Channel sftpChannel = null;
/**
* 获取连接
*/
public ChannelSftp getChannel(String username, String password, String host, int port) {
if (port <= 0) {
port = 22;
}
try {
// 创建 JSch 对象
JSch jSch = new JSch();
// 根据用户名,主机 ip,端口获取一个 session 对象
session = jSch.getSession(username, host, port);
if (StringUtils.isNotBlank(password)) {
// 设置密码
session.setPassword(password);
}
Properties config = new Properties();
// ???
config.put("StrictHostKeyChecking", "no");
// 为 session 设置 properties
session.setConfig(config);
session.setTimeout(5000);
// 通过 session 建立连接
session.connect();
// 打开 sftp 通道
sftpChannel = session.openChannel("sftp");
// 建立 SFTP 通道的连接
sftpChannel.connect();
} catch (JSchException e) {
e.printStackTrace();
}
return (ChannelSftp) sftpChannel;
}
// src 和 dst 填文件的全路径地址
public void downloadFile(ChannelSftp sftp, String src, String dst) {
try {
FileOutputStream fileOutputStream = new FileOutputStream(dst);
InputStream inputStream = sftp.get(src);
byte[] bytes = new byte[64];
int len;
assert inputStream != null;
while ((len = inputStream.read(bytes)) != -1) {
fileOutputStream.write(bytes, 0, len);
}
} catch (IOException | SftpException e) {
e.printStackTrace();
}
}
public void closeChannel() {
if (Objects.nonNull(session)) {
session.disconnect();
}
if (Objects.nonNull(sftpChannel)) {
sftpChannel.disconnect();
}
}
}
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
# 解析 Excel
# POI
# 依赖
引入 Apache 的 POI 依赖:
只需要导入 poi-ooxml 依赖就可以,因为这个依赖里集成了 poi。
<!-- 只要引入这个 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.0.0</version>
</dependency>
2
3
4
5
6
<!-- poi-ooxml 依赖中已经集成了这个依赖,所以只需要引入上面的 poi-ooxml 依赖就可以了 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.0.0</version>
</dependency>
2
3
4
5
6
之所以引入 poi-ooxml 而不是 poi,是因为涉及到流只能读取一次的特性这个问题,poi-ooxml 依赖中对这块做了处理。
# 代码
Workbook workbook = null;
InputStream inputStream = null;
try {
// 不用手动判断 Is file exist,在 new FileInputStream("./aaa.xlsx"); 这个构造方法内部其实已经做了判断。
// File file = new File("./aaa.xlsx");
// if (file.exists()) {
// inputStream = new FileInputStream(file);
// }
inputStream = new FileInputStream("./aaa.xlsx");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
try {
// 以前是要根据不同 excel 创建不同对象:
// Excel2003 版本 -> HSSFWorkbook, Excel2007 版本 -> XSSFWorkbook
// 在这里直接使用 WorkbookFactory.create 就可以从提供的输入中自动检测来创建适当类型的工作簿(无论是 HSSFWorkbook 还是 XSSFWorkbook)
workbook = WorkbookFactory.create(inputStream);
// 根据页面index 获取sheet页
Sheet sheet = workbook.getSheetAt(0);
Row row = null;
for (int i = 0; i < sheet.getPhysicalNumberOfRows(); i++) {
// 获取每一行数据
row = sheet.getRow(i);
// 获取单元格
// row.getPhysicalNumberOfCells(); 这是非空的单元格数(一行中)
for (int j = 0; j < row.getLastCellNum(); j++) {
Cell cell = row.getCell(j);
if (cell == null) {
System.out.printf("%35s", "");
} else {
String cellContent = cell.toString();
System.out.printf("%35s", cellContent);
}
if (j == row.getLastCellNum() - 1) {
System.out.println("\n");
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
assert inputStream != null;
inputStream.close();
assert workbook != null;
workbook.close();
} catch (IOException e) {
e.printStackTrace();
}
}
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
# 解析 Excel 到 List
思路:将 Excel 的每一行的数据赋值到一个类的对象上,然后整个 Excel 映射到一个 List 中。
# 解析大文件的 OOM 问题
POI 的本质是将 Excel 文件解析放到内存中,那么如果一个文件特别大,将会遇到 OOM 问题。
# EasyExcel
阿里巴巴出品,解决了 OOM 问题。
# 依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
2
3
4
5
实际上这个依赖集成了 apache poi
# 读文件代码
定义一个 model
@Data public class UserExcel { @ExcelProperty(index = 0) private String name; @ExcelProperty(index = 1) private String age; @ExcelProperty(index = 2) private String gender; }
1
2
3
4
5
6
7
8
9
10
11
12
13JDK8+ ,不用额外写一个 Listener
String fileName = "./user.xlsx"; EasyExcel.read(fileName, UserExcel.class, new PageReadListener<UserExcel>(dataList -> { for (UserExcel userExcel : dataList) { // userExcel 就拿到了,默认每次读出 100 条 } })).sheet().doRead();
1
2
3
4
5
6
# 写文件代码
需求: 调用 controller 下载 excel。
定义一个 model
@Data public class UserExcel { @ExcelProperty(value = "名字", index = 0) private String name; @ExcelProperty(value = "年龄",index = 1) private String age; @ExcelProperty(value = "性别",index = 2) private String gender; }
1
2
3
4
5
6
7
8
9
10
11
12编写代码
List<UserExcel> data = new ArrayList<>(); EasyExcel.write(fileName, UserExcel.class) .sheet("模板") .doWrite(data); --- 将文件通过 controller 输出 // 设置头 response.setContentType("application/vnd.ms-excel;charset=utf-8"); //test.xls是弹出下载对话框的文件名,不能为中文,中文请自行编码 response.setHeader("Content-Disposition", "attachment;filename=test.xlsx"); // 将文件写入 response File file = new File(fileName); ServletOutputStream outputStream = response.getOutputStream(); try (InputStream inputStream = new FileInputStream(file)) { byte[] bytes = new byte[1024]; int count = -1; while ((count = inputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, count); } outputStream.flush(); outputStream.close(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 讨论区
由于评论过多会影响页面最下方的导航,故将评论区做默认折叠处理。