天天看點

response.text不顯示body内容_Spring-Boot-admin之Httptrace顯示入參和出參及Redisson監控

response.text不顯示body内容_Spring-Boot-admin之Httptrace顯示入參和出參及Redisson監控

spring-boot-admin

(以下簡稱

SBA

)與Spring Boot、Spring cloud項目以starter得方式自動內建,包括Server端和Client端

SBA

監控包括應用的基本資訊、logfile(線上實時浏覽或者download)、JVM資訊(線程資訊、堆資訊、非堆資訊)、Web(API接口資訊、最近100次API調用的資訊)、應用中使用者登入資訊;監控名額很全面,但針對具體項目就要增加符合自己項目的内容了,比如如下兩點:

自定義HttpTrace增加入參和出參

結果:

response.text不顯示body内容_Spring-Boot-admin之Httptrace顯示入參和出參及Redisson監控

spring-boot-admin

中HttpTrace顯示的資訊包括session、principal、request、response、timeTaken和timestamp,但session、principal對該項目完全無用,request是

HttpTrace

的内部類顯示資訊包括:

1private final String method;
2private final URI uri;
3//唯一可以擴充的地方
4private final Map<String, List<String>> headers;
5private final String remoteAddress;
           

response也是

HttpTrace

的内部類:

1private final int status;
2//唯一可以擴充的地方
3private final Map<String, List<String>> headers;
           

唯一缺少的就是請求的

入參

出參

,而Headers的資訊是無用的。是以擴充HttpTrace顯示請求中的

入參

出參

勢在必行,大緻的思路是:自定義Filter-->裝飾模式轉換成自定義的request和response對象,内部擷取請求和相應内容-->HttpExchangeTracer建立HttpTrace對象-->InmemoryHttpTraceRepository儲存100次請求的HttpTrace對象,供server端使用。由于Filter中使用的部分對象是先建立的是以我們先從需要的零部件開始

  • 第一步:包裝HttpServletRequest擷取請求内容:
1public class RequestWrapper extends HttpServletRequestWrapper {
 2//存放請求的消息體(先緩存一份)
 3    private byte[] body;
 4//自定義輸入流的包裝類,将緩存資料再寫入到流中
 5    private ServletInputStreamWrapper wrapper;
 6    private final Logger logger = LoggerFactory.getLogger(RequestWrapper.class);
 7
 8    public RequestWrapper(HttpServletRequest request) {
 9        super(request);
10        try {
11//使用Apache的commons-io工具從request中先讀取資料
12            body = IOUtils.toByteArray(request.getInputStream());
13        } catch (IOException e) {
14            logger.error("從請求中擷取請求參數出現異常:", e);
15        }
16//将讀取出來的記憶體再寫入流中
17        wrapper = new ServletInputStreamWrapper(new ByteArrayInputStream(body));
18    }
19//轉換成String 供外部調用,并替換轉義字元
20    public String body() {
21        return new String(body).replaceAll("[ntr]","");
22    }
23//将我們的自定義的流包裝類傳回,供系統調用 讀取資料
24    @Override
25    public ServletInputStream getInputStream() throws IOException {
26        return this.wrapper;
27    }
28//将我們的自定義的流包裝類傳回,供系統調用 讀取資料
29    @Override
30    public BufferedReader getReader() throws IOException {
31        return new BufferedReader(new InputStreamReader(this.wrapper));
32    }
33   //從給定的輸入流中讀取資料
34    static final class ServletInputStreamWrapper extends ServletInputStream {
35
36        private InputStream inputStream;
37
38        public ServletInputStreamWrapper(InputStream inputStream) {
39            this.inputStream = inputStream;
40        }
41
42        @Override
43        public boolean isFinished() {
44            return true;
45        }
46
47        @Override
48        public boolean isReady() {
49            return false;
50        }
51
52        @Override
53        public void setReadListener(ReadListener listener) {
54
55        }
56//讀取緩存資料
57        @Override
58        public int read() throws IOException {
59            return this.inputStream.read();
60        }
61
62        public InputStream getInputStream() {
63            return inputStream;
64        }
65
66        public void setInputStream(InputStream inputStream) {
67            this.inputStream = inputStream;
68        }
69    }
70}
           
  • 第二步:包裝HttpServletResponse類擷取響應内容:
1public class ResponseWrapper extends HttpServletResponseWrapper {
 2
 3    private HttpServletResponse response;
 4//緩存響應内容的輸出流
 5    private ByteArrayOutputStream result = new ByteArrayOutputStream();
 6
 7    public ResponseWrapper(HttpServletResponse response) {
 8        super(response);
 9        this.response = response;
10    }
11
12    /**
13     * 響應的内容 供外部調用
14     *針對 體積較大的響應内容 很容易發生 OOM(比如:/actuator/logfile 接口),可在調用該方法的地方就行api過濾
15     *解決方法在第四步
16     */
17    public String body(){
18        return result.toString();
19    }
20
21    @Override
22    public ServletOutputStream getOutputStream() throws IOException {
23        return new ServletOutputStreamWrapper(this.response,this.result);
24    }
25
26    @Override
27    public PrintWriter getWriter() throws IOException {
28        return new PrintWriter(new OutputStreamWriter(this.result,this.response.getCharacterEncoding()));
29    }
30
31//自定義輸出流的包裝類 内部類
32    static final class ServletOutputStreamWrapper extends ServletOutputStream{
33
34        private HttpServletResponse response;
35        private ByteArrayOutputStream byteArrayOutputStream;
36
37        public ServletOutputStreamWrapper(HttpServletResponse response, ByteArrayOutputStream byteArrayOutputStream) {
38            this.response = response;
39            this.byteArrayOutputStream = byteArrayOutputStream;
40        }
41
42        @Override
43        public boolean isReady() {
44            return true;
45        }
46
47        @Override
48        public void setWriteListener(WriteListener listener) {
49
50        }
51
52        @Override
53        public void write(int b) throws IOException {
54            this.byteArrayOutputStream.write(b);
55        }
56
57        /**
58         * 将内容重新重新整理到傳回的對象中  并且避免多次重新整理
59         */
60        @Override
61        public void flush() throws IOException {
62            if(!response.isCommitted()){
63                byte[] bytes = this.byteArrayOutputStream.toByteArray();
64                ServletOutputStream outputStream = response.getOutputStream();
65                outputStream.write(bytes);
66                outputStream.flush();
67            }
68        }
69    }
70}
           
  • 第三步:擴充

    TraceableRequest

    ,該接口中的方法會在建立

    HttpTrace#Request

    内部類時調用,自定義實作裡面的方法,再在過濾器中引用該類就可以達到自定義顯示内容的目的, 該類中的Request是我們第一步建立的裝飾類,不能使用HttpServletRequest
1public class CustomerTraceableRequest implements TraceableRequest {
 2//自定義的Request裝飾類,不能使用HttpServletRequest
 3    private RequestWrapper request;
 4
 5    public CustomerTraceableRequest(RequestWrapper request) {
 6        this.request = request;
 7    }
 8//HttpTrace類中getMethod會調用
 9    @Override
10    public String getMethod() {
11        return request.getMethod();
12    }
13
14    /**
15     * @return POST 或者 GET 方式 都傳回 {ip}:{port}/uir的形式傳回
16     */
17    @Override
18    public URI getUri() {
19        return URI.create(request.getRequestURL().toString());
20    }
21
22//因為在HttpTrace中可擴充的隻有headers的Map,是以我們自定義屬性RequestParam存入headers中,作為入參資訊展示
23    @Override
24    public Map<String, List<String>> getHeaders() {
25        Map<String, List<String>> headerParam = new HashMap<>(1);
26        headerParam.put("RequestParam",getParams());
27        return headerParam;
28    }
29
30//該方法也要重寫,預設的太簡單無法擷取真是的IP
31    @Override
32    public String getRemoteAddress() {
33        return IpUtils.getIpAddress(request);
34    }
35//根據GET或者POST的請求方式不同,擷取不同情況下的請求參數
36    public List<String> getParams() {
37        String params = null;
38        String method = this.getMethod();
39        if(HttpMethod.GET.matches(method)){
40            params = request.getQueryString();
41        }else if(HttpMethod.POST.matches(method)){
42            params = this.request.body();
43        }
44        List<String> result = new ArrayList<>(1);
45        result.add(params);
46        return result;
47    }
48}
           
  • 第四步:擴充

    TraceableResponse

    ,該接口中方法在建立

    HttpTrace#Response

    内部類時引用,自定義實作裡面的方法:
1public class CustomerTraceableResponse implements TraceableResponse {
 2    //自定義的HttpServletResponse包裝類
 3    private ResponseWrapper response;
 4    private HttpServletRequest request;
 5
 6    public CustomerTraceableResponse(ResponseWrapper response, HttpServletRequest request) {
 7        this.response = response;
 8        this.request = request;
 9    }
10//傳回響應狀态
11    @Override
12    public int getStatus() {
13        return response.getStatus();
14    }
15//擴充Response headers添加Response Body屬性,展示響應内容,但是需要排除`/actuator/`開頭的請求,這裡面部分響應内容太大,容易OOM
16    @Override
17    public Map<String, List<String>> getHeaders() {
18        if(isActuatorUri()){
19            return extractHeaders();
20        }else{
21            Map<String, List<String>> result = new LinkedHashMap<>(1);
22            List<String> responseBody = new ArrayList<>(1);
23            responseBody.add(this.response.body());
24            result.put("ResponseBody", responseBody);
25            result.put("Content-Type", getContentType());
26            return result;
27        }
28    }
29//是否是需要過濾的請求uri
30    private boolean isActuatorUri() {
31        String requestUri = request.getRequestURI();
32        AntPathMatcher matcher = new AntPathMatcher();
33        return matcher.match("/actuator/**", requestUri);
34    }
35//server端頁面展示的Content-Type以及Length是從Response中擷取的
36    private List<String> getContentType() {
37        List<String> list = new ArrayList<>(1);
38        list.add(this.response.getContentType());
39        return list;
40    }
41//針對/actuator/**的請求傳回預設的headers内容獲
42    private Map<String, List<String>> extractHeaders() {
43        Map<String, List<String>> headers = new LinkedHashMap<>();
44        for (String name : this.response.getHeaderNames()) {
45            headers.put(name, new ArrayList<>(this.response.getHeaders(name)));
46        }
47        return headers;
48    }
49}
           
  • 第五步:自定義

    Filter

    對Resquest和Response過濾,并建立HttpTrace對象:
1public class CustomerHttpTraceFilter extends OncePerRequestFilter implements Ordered {
 2//存儲HttpTrace的repository,預設是居于記憶體的,可擴充該類跟換存儲資料的方式
 3    private HttpTraceRepository httpTraceRepository;
 4//該類建立HttpTrace對象,Set<Include>包含的内容是我們需要展示那些内容的容器(request-headers,response-headers,remote-address,time-taken)
 5    private HttpExchangeTracer httpExchangeTracer;
 6
 7    public CustomerHttpTraceFilter(HttpTraceRepository httpTraceRepository, HttpExchangeTracer httpExchangeTracer) {
 8        this.httpTraceRepository = httpTraceRepository;
 9        this.httpExchangeTracer = httpExchangeTracer;
10    }
11
12    @Override
13    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
14//校驗URI是否有效
15        if (!isRequestValid(request)) {
16            filterChain.doFilter(request, response);
17            return;
18        }
19//将HttpServletRequest包裝成我們自己的
20        RequestWrapper wrapper = new RequestWrapper(request);
21//将HttpServletResponse包裝成我們的自己的
22        ResponseWrapper responseWrapper = new ResponseWrapper(response);
23
24//建立我們的自己的TraceRequest對象
25        CustomerTraceableRequest traceableRequest = new CustomerTraceableRequest(wrapper);
26//建立HttpTrace對象(FilteredTraceableRequest 是内部類,通過Set<Include>篩選那些資訊需要展示就儲存那些資訊),重點設定HttpTrace#Request對象的各種參數
27        HttpTrace httpTrace = httpExchangeTracer.receivedRequest(traceableRequest);
28        try {
29            filterChain.doFilter(wrapper, responseWrapper);
30        } finally {
31//自定義的TraceableResponse 儲存需要的response資訊
32            CustomerTraceableResponse traceableResponse = new CustomerTraceableResponse(responseWrapper,request);
33//根據Set<Include>設定HttpTrace中session、principal、timeTaken資訊以及Response内部類資訊       
34  this.httpExchangeTracer.sendingResponse(httpTrace, traceableResponse, null, null);
35//将HttpTrace對象儲存在Respository中存儲起來
36            this.httpTraceRepository.add(httpTrace);
37        }
38    }
39
40    private boolean isRequestValid(HttpServletRequest request) {
41        try {
42            new URI(request.getRequestURL().toString());
43            return true;
44        } catch (URISyntaxException ex) {
45            return false;
46        }
47    }
48
49    @Override
50    public int getOrder() {
51        return Ordered.LOWEST_PRECEDENCE - 10;
52    }
53}
           
  • 第六步:通過

    @SpringBootApplication(exclude)

    禁用

    HttpTraceAutoConfiguration

    自動配置,自定義自動配置更換Filter過濾器:
[email protected]
 [email protected]
 [email protected](prefix = "management.trace.http", name = "enabled", matchIfMissing = true)
 [email protected](HttpTraceProperties.class)
 5public class TraceFilterConfig {
 6
 7//存儲HttpTrace資訊的對象
 8    @Bean
 9    @ConditionalOnMissingBean(HttpTraceRepository.class)
10    public InMemoryHttpTraceRepository traceRepository() {
11        return new InMemoryHttpTraceRepository();
12    }
13//建立HttpTrace對象Exchange
14    @Bean
15    @ConditionalOnMissingBean
16    public HttpExchangeTracer httpExchangeTracer(HttpTraceProperties traceProperties) {
17        return new HttpExchangeTracer(traceProperties.getInclude());
18    }
19
20    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
21    static class ServletTraceFilterConfiguration {
22//将我們自定義的Filter已Bean的方式注冊,才能生效
23        @Bean
24        @ConditionalOnMissingBean
25        public CustomerHttpTraceFilter httpTraceFilter(HttpTraceRepository repository,
26                                               HttpExchangeTracer tracer) {
27            return new CustomerHttpTraceFilter(repository,tracer);
28        }
29    }
30
31    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
32    static class ReactiveTraceFilterConfiguration {
33
34        @Bean
35        @ConditionalOnMissingBean
36        public HttpTraceWebFilter httpTraceWebFilter(HttpTraceRepository repository,
37                                                     HttpExchangeTracer tracer, HttpTraceProperties traceProperties) {
38            return new HttpTraceWebFilter(repository, tracer,
39                    traceProperties.getInclude());
40        }
41    }
42}
           

內建Redisson健康狀态監控

如果有引入

spring-boot-starter-redis

,SBA預設同過

RedisConnectionFactory

監控Redis的健康狀态,無奈Redisson還沒有,自己東收豐衣足食。通過

HealthIndicator

ReactiveHealthIndicator

使用政策模式實作不同元件的健康監控,後者是使用Rective模式下的。我是通過JavaBean的方式配置Redisson,是以順便實作

ReactiveHealthIndicator

再添加該名額即可:

[email protected]
 [email protected](value = RedissonProperties.class)
 3public class RedissonConfig implements ReactiveHealthIndicator {
 4//自己的RedissonProperties檔案
 5    @Autowired
 6    private RedissonProperties redissonProperties;
 7//暴露 redissonClient句柄
 8    @Bean
 9    @ConditionalOnMissingBean
10    public RedissonClient redisClient() {
11        return Redisson.create(config());
12    }
13//通過Bean的方式配置RedissonConfig相關資訊
14    @Bean
15    public Config config() {
16        Config config = new Config();
17        config.useSingleServer() //單實列模式
18                .setAddress(redissonProperties.getAddress() + ":" + redissonProperties.getPort())
19                .setPassword(redissonProperties.getPassword())
20                .setDatabase(redissonProperties.getDatabase())
21                .setConnectionPoolSize(redissonProperties.getConnectionPoolSize())
22                .setConnectionMinimumIdleSize(redissonProperties.getConnectionMinimumIdleSize())
23                .setIdleConnectionTimeout(redissonProperties.getIdleConnectionTimeout())
24                .setSubscriptionConnectionPoolSize(redissonProperties.getSubscriptionConnectionPoolSize())
25                .setSubscriptionConnectionMinimumIdleSize(redissonProperties.getSubscriptionConnectionMinimumIdleSize())
26                .setTimeout(redissonProperties.getTimeout())
27                .setRetryAttempts(redissonProperties.getRetryAttempts())
28                .setRetryInterval(redissonProperties.getRetryInterval())
29                .setConnectTimeout(redissonProperties.getConnectTimeout())
30                .setReconnectionTimeout(redissonProperties.getReconnectionTimeout());
31        config
32                .setCodecProvider(new DefaultCodecProvider())
33                .setEventLoopGroup(new NioEventLoopGroup())
34                .setThreads(Runtime.getRuntime().availableProcessors() * 2)
35                .setNettyThreads(Runtime.getRuntime().availableProcessors() * 2);
36        return config;
37    }
38//實作ReactiveHealthIndicator 重寫health方法
39    @Override
40    public Mono<Health> health() {
41        return checkRedissonHealth().onErrorResume(ex -> Mono.just(new Health.Builder().down(ex).build()));
42    }
43//我是通過ping 的方式判斷redis伺服器是否up的狀态,并增加加Netty和Threads的監控
44    private Mono<Health> checkRedissonHealth() {
45        Health.Builder builder = new Health.Builder();
46        builder.withDetail("address", redissonProperties.getAddress());
47        //檢測健康狀态
48        if (this.redisClient().getNodesGroup().pingAll()) {
49            builder.status(Status.UP);
50            builder.withDetail("dataBase", redissonProperties.getDatabase());
51            builder.withDetail("redisNodeThreads", this.redisClient().getConfig().getThreads());
52            builder.withDetail("nettyThreads", this.redisClient().getConfig().getNettyThreads());
53
54        }else{
55            builder.status(Status.DOWN);
56        }
57        return Mono.just(builder.build());
58    }
59}
           

在頁面上看就是:

response.text不顯示body内容_Spring-Boot-admin之Httptrace顯示入參和出參及Redisson監控

Ok!圓滿完成!

如有錯誤,不吝賜教!

歡迎關注Java技術公衆号:

response.text不顯示body内容_Spring-Boot-admin之Httptrace顯示入參和出參及Redisson監控