gateway版本是 2.0.1
1.pom結構
(部分內部項目依賴已經隱藏)
1
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
|
< dependency > < groupId >org.springframework.cloud</ groupId > < artifactId >spring-cloud-starter-netflix-eureka-client</ artifactId > </ dependency > < dependency > < groupId >org.springframework.cloud</ groupId > < artifactId >spring-cloud-starter-gateway</ artifactId > </ dependency > <!--監控相關--> < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-actuator</ artifactId > </ dependency > <!-- redis --> <!--<dependency>--> <!--<groupId>org.springframework.boot</groupId>--> <!--<artifactId>spring-boot-starter-data-redis</artifactId>--> <!--</dependency>--> <!-- test-scope --> < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-test</ artifactId > < scope >test</ scope > </ dependency > < dependency > < groupId >ch.qos.logback</ groupId > < artifactId >logback-core</ artifactId > < version >1.1.11</ version > </ dependency > < dependency > < groupId >ch.qos.logback</ groupId > < artifactId >logback-classic</ artifactId > < version >1.1.11</ version > </ dependency > < dependency > < groupId >org.apache.httpcomponents</ groupId > < artifactId >httpclient</ artifactId > < version >4.5.6</ version > </ dependency > <!--第三方的jdbctemplatetool--> < dependency > < groupId >org.crazycake</ groupId > < artifactId >jdbctemplatetool</ artifactId > < version >1.0.4-RELEASE</ version > </ dependency > < dependency > < groupId >mysql</ groupId > < artifactId >mysql-connector-java</ artifactId > </ dependency > <!-- alibaba start --> < dependency > < groupId >com.alibaba</ groupId > < artifactId >druid</ artifactId > </ dependency > |
2.表結構
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
CREATE TABLE `zc_log_notes` ( `id` int (11) NOT NULL AUTO_INCREMENT COMMENT '日志信息記錄表主鍵id' , `notes` varchar (255) DEFAULT NULL COMMENT '操作記錄信息' , `amenu` varchar (255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '一級菜單' , `bmenu` varchar (255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '二級菜單' , `ip` varchar (255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人ip地址,先用varchar存' , `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '請求值' , `response` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '返回值' , `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作時間' , `create_user` int (11) DEFAULT NULL COMMENT '操作人id' , `end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '響應時間' , `status` int (1) NOT NULL DEFAULT '1' COMMENT '響應結果1成功0失敗' , PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8mb4 COLLATE =utf8mb4_0900_ai_ci COMMENT= '日志信息記錄表' ; |
3.實體結構
1
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
|
@Table (catalog = "zhiche" , name = "zc_log_notes" ) public class LogNotes { /** * 日志信息記錄表主鍵id */ private Integer id; /** * 操作記錄信息 */ private String notes; /** * 一級菜單 */ private String amenu; /** * 二級菜單 */ private String bmenu; /** * 操作人ip地址,先用varchar存 */ private String ip; /** * 請求參數記錄 */ private String params; /** * 返回結果記錄 */ private String response; /** * 操作時間 */ private Date createTime; /** * 操作人id */ private Integer createUser; /** * 響應時間 */ private Date endTime; /** * 響應結果1成功0失敗 */ private Integer status; @Id @GeneratedValue (strategy = GenerationType.IDENTITY) public Integer getId() { return id; } public void setId(Integer id) { this .id = id; } public String getNotes() { return notes; } public void setNotes(String notes) { this .notes = notes; } public String getAmenu() { return amenu; } public void setAmenu(String amenu) { this .amenu = amenu; } public String getBmenu() { return bmenu; } public void setBmenu(String bmenu) { this .bmenu = bmenu; } public String getIp() { return ip; } public void setIp(String ip) { this .ip = ip; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this .createTime = createTime; } public Integer getCreateUser() { return createUser; } public void setCreateUser(Integer createUser) { this .createUser = createUser; } public Date getEndTime() { return endTime; } public void setEndTime(Date endTime) { this .endTime = endTime; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this .status = status; } public String getParams() { return params; } public void setParams(String params) { this .params = params; } public String getResponse() { return response; } public void setResponse(String response) { this .response = response; } public void setAppendResponse(String response){ if (StringUtils.isNoneBlank( this .response)) { this .response = this .response + response; } else { this .response = response; } } } |
4.dao層和Service層省略..
5.filter代碼
1. RequestRecorderGlobalFilter 實現了GlobalFilter和Order
1
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
package com.zc.gateway.filter; import com.zc.entity.LogNotes; import com.zc.gateway.service.FilterService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.URI; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** * @author qiwenshuai * @note 目前只記錄了request方式為POST請求的方式 * @since 19-5-16 17:29 by jdk 1.8 */ @Component public class RequestRecorderGlobalFilter implements GlobalFilter, Ordered { @Autowired FilterService filterService; private Logger logger = LoggerFactory.getLogger(RequestRecorderGlobalFilter. class ); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest originalRequest = exchange.getRequest(); URI originalRequestUrl = originalRequest.getURI(); //只記錄http的請求 String scheme = originalRequestUrl.getScheme(); if ((! "http" .equals(scheme) && ! "https" .equals(scheme))) { return chain.filter(exchange); } //這是我要打印的log-StringBuilder StringBuilder logbuilder = new StringBuilder(); //我自己的log實體 LogNotes logNotes = new LogNotes(); // 返回解碼 RecorderServerHttpResponseDecorator response = new RecorderServerHttpResponseDecorator(exchange.getResponse(), logNotes, filterService); //請求解碼 RecorderServerHttpRequestDecorator recorderServerHttpRequestDecorator = new RecorderServerHttpRequestDecorator(exchange.getRequest()); //增加過濾攔截吧 ServerWebExchange ex = exchange.mutate() .request(recorderServerHttpRequestDecorator) .response(response) .build(); // 觀察者模式 打印一下請求log // 這里可以在 配置文件中我進行配置 // if (logger.isDebugEnabled()) { response.beforeCommit(() -> Mono.defer(() -> printLog(logbuilder, response))); // } return recorderOriginalRequest(logbuilder, ex, logNotes) .then(chain.filter(ex)) .then(); } private Mono<Void> recorderOriginalRequest(StringBuilder logBuffer, ServerWebExchange exchange, LogNotes logNotes) { logBuffer.append(System.currentTimeMillis()) .append( "------------" ); ServerHttpRequest request = exchange.getRequest(); Mono<Void> result = recorderRequest(request, logBuffer.append( "\n原始請求:\n" ), logNotes); try { filterService.addLog(logNotes); } catch (Exception e) { logger.error( "保存請求參數出現錯誤, e->{}" , e.getMessage()); } return result; } /** * 記錄原始請求邏輯 */ private Mono<Void> recorderRequest(ServerHttpRequest request, StringBuilder logBuffer, LogNotes logNotes) { URI uri = request.getURI(); HttpMethod method = request.getMethod(); HttpHeaders headers = request.getHeaders(); logNotes.setIp(headers.getHost().getHostString()); logNotes.setAmenu( "一級菜單" ); logNotes.setBmenu( "二級菜單" ); logNotes.setNotes( "操作記錄" ); logBuffer .append(method.toString()).append( ' ' ) .append(uri.toString()).append( '\n' ); logBuffer.append( "------------請求頭------------\n" ); headers.forEach((name, values) -> { values.forEach(value -> { logBuffer.append(name).append( ":" ).append(value).append( '\n' ); }); }); Charset bodyCharset = null ; if (hasBody(method)) { long length = headers.getContentLength(); if (length <= 0 ) { logBuffer.append( "------------無body------------\n" ); } else { logBuffer.append( "------------body 長度:" ).append(length).append( " contentType:" ); MediaType contentType = headers.getContentType(); if (contentType == null ) { logBuffer.append( "null,不記錄body------------\n" ); } else if (!shouldRecordBody(contentType)) { logBuffer.append(contentType.toString()).append( ",不記錄body------------\n" ); } else { bodyCharset = getMediaTypeCharset(contentType); logBuffer.append(contentType.toString()).append( "------------\n" ); } } } if (bodyCharset != null ) { return doRecordReqBody(logBuffer, request.getBody(), bodyCharset, logNotes) .then(Mono.defer(() -> { logBuffer.append( "\n------------ end ------------\n\n" ); return Mono.empty(); })); } else { logBuffer.append( "------------ end ------------\n\n" ); return Mono.empty(); } } //日志輸出返回值 private Mono<Void> printLog(StringBuilder logBuilder, ServerHttpResponse response) { HttpStatus statusCode = response.getStatusCode(); assert statusCode != null ; logBuilder.append( "響應:" ).append(statusCode.value()).append( " " ).append(statusCode.getReasonPhrase()).append( '\n' ); HttpHeaders headers = response.getHeaders(); logBuilder.append( "------------響應頭------------\n" ); headers.forEach((name, values) -> { values.forEach(value -> { logBuilder.append(name).append( ":" ).append(value).append( '\n' ); }); }); logBuilder.append( "\n------------ end at " ) .append(System.currentTimeMillis()) .append( "------------\n\n" ); logger.info(logBuilder.toString()); return Mono.empty(); } // @Override public int getOrder() { //在GatewayFilter之前執行 return - 1 ; } private boolean hasBody(HttpMethod method) { //只記錄這3種謂詞的body // if (method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH) return true ; // return false; } //記錄簡單的常見的文本類型的request的body和response的body private boolean shouldRecordBody(MediaType contentType) { String type = contentType.getType(); String subType = contentType.getSubtype(); if ( "application" .equals(type)) { return "json" .equals(subType) || "x-www-form-urlencoded" .equals(subType) || "xml" .equals(subType) || "atom+xml" .equals(subType) || "rss+xml" .equals(subType); } else if ( "text" .equals(type)) { return true ; } //暫時不記錄form return false ; } // 獲取請求的參數 private Mono<Void> doRecordReqBody(StringBuilder logBuffer, Flux<DataBuffer> body, Charset charset, LogNotes logNotes) { return DataBufferUtils.join(body).doOnNext(buffer -> { CharBuffer charBuffer = charset.decode(buffer.asByteBuffer()); //記錄我實體的請求體 logNotes.setParams(charBuffer.toString()); logBuffer.append(charBuffer.toString()); DataBufferUtils.release(buffer); }).then(); } private Charset getMediaTypeCharset( @Nullable MediaType mediaType) { if (mediaType != null && mediaType.getCharset() != null ) { return mediaType.getCharset(); } else { return StandardCharsets.UTF_8; } } } |
2.RecorderServerHttpRequestDecorator 繼承了ServerHttpRequestDecorator
1
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
|
package com.zc.gateway.filter; import com.zc.entity.LogNotes; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.LinkedList; import java.util.List; /** * @author qiwenshuai * @note * @since 19-5-16 17:30 by jdk 1.8 */ // request public class RecorderServerHttpRequestDecorator extends ServerHttpRequestDecorator { private final List<DataBuffer> dataBuffers = new LinkedList<>(); private boolean bufferCached = false ; private Mono<Void> progress = null ; public RecorderServerHttpRequestDecorator(ServerHttpRequest delegate) { super (delegate); } //重寫request請求體 @Override public Flux<DataBuffer> getBody() { synchronized (dataBuffers) { if (bufferCached) return copy(); if (progress == null ) { progress = cache(); } return progress.thenMany(Flux.defer( this ::copy)); } } private Flux<DataBuffer> copy() { return Flux.fromIterable(dataBuffers) .map(buf -> buf.factory().wrap(buf.asByteBuffer())); } private Mono<Void> cache() { return super .getBody() .map(dataBuffers::add) .then(Mono.defer(()-> { bufferCached = true ; progress = null ; return Mono.empty(); })); } } |
3.RecorderServerHttpResponseDecorator 繼承了 ServerHttpResponseDecorator
1
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
|
package com.zc.gateway.filter; import com.zc.entity.LogNotes; import com.zc.gateway.service.FilterService; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import java.nio.charset.Charset; import java.util.LinkedList; import java.util.List; /** * @author qiwenshuai * @note * @since 19-5-16 17:32 by jdk 1.8 */ public class RecorderServerHttpResponseDecorator extends ServerHttpResponseDecorator { private Logger logger = LoggerFactory.getLogger(RecorderServerHttpResponseDecorator. class ); private LogNotes logNotes; private FilterService filterService; RecorderServerHttpResponseDecorator(ServerHttpResponse delegate, LogNotes logNotes, FilterService filterService) { super (delegate); this .logNotes = logNotes; this .filterService = filterService; } /** * 基于netty,我這里需要顯示的釋放一次dataBuffer,但是slice出來的byte是不需要釋放的, * 與下層共享一個字符串緩沖池,gateway過濾器使用的是nettyWrite類,會發生response數據多次才能返回完全。 * 在 ServerHttpResponseDecorator 之后會釋放掉另外一個refCount. */ @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { DataBufferFactory bufferFactory = this .bufferFactory(); if (body instanceof Flux) { Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body; Publisher<? extends DataBuffer> re = fluxBody.map(dataBuffer -> { // probably should reuse buffers byte [] content = new byte [dataBuffer.readableByteCount()]; // 數據讀入數組 dataBuffer.read(content); // 釋放掉內存 DataBufferUtils.release(dataBuffer); // 記錄返回值 String s = new String(content, Charset.forName( "UTF-8" )); logNotes.setAppendResponse(s); try { filterService.updateLog(logNotes); } catch (Exception e) { logger.error( "Response值修改日志記錄出現錯誤->{}" , e); } byte [] uppedContent = new String(content, Charset.forName( "UTF-8" )).getBytes(); return bufferFactory.wrap(uppedContent); }); return super .writeWith(re); } return super .writeWith(body); } @Override public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) { return writeWith(Flux.from(body).flatMapSequential(p -> p)); } } |
注意:
網關過濾返回值 底層用到了Netty服務,在response返回的時候,有時候會寫的數據是不全的,于是我在實體類中新增了一個setAppendResponse方法進行拼接, 再者,gateway的過濾器是鏈式結構,需要定義order排序為最先(-1),然后和預置的gateway過濾器做一個combine.
代碼中用到的 dataBuffer 結構,底層其實也是類似netty的byteBuffer,用到了字節數組池,同時也用到了 引用計數器 (refInt).
為了讓jvm在gc的時候垃圾得到回收,避免內存泄露,我們需要在轉換字節使用的地方,顯示的釋放一次
1
|
DataBufferUtils.release(dataBuffer); |
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/gpdsjqws/article/details/90437732