How Spring Cloud gateway Gateway Intercepts Post Request Log

  • 2021-10-27 07:49:47
  • OfStack

gateway version 2.0. 1

1. pom structure

(Some internal project dependencies are hidden)


<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>
<!-- Monitoring related -->
<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>
<!-- No. 1 3 Square 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. Table structure


CREATE TABLE `zc_log_notes` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT ' Log information record table primary key id',
  `notes` varchar(255) DEFAULT NULL COMMENT ' Operation record information ',
  `amenu` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '1 Level menu ',
  `bmenu` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '2 Level menu ',
  `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT ' Operator ip Address, first use varchar Deposit ',
  `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT ' Request value ',
  `response` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT ' Return value ',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ' Operation time ',
  `create_user` int(11) DEFAULT NULL COMMENT ' Operator id',
  `end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ' Response time ',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT ' Response result 1 Success 0 Failure ',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT=' Log information record table ';

3. Physical structure


@Table(catalog = "zhiche", name = "zc_log_notes")
public class LogNotes {
    /**
     *  Log information record table primary key id
     */
    private Integer id;
    /**
     *  Operation record information 
     */
    private String notes;
    /**
     * 1 Level menu 
     */
    private String amenu;
    /**
     * 2 Level menu 
     */
    private String bmenu;
    /**
     *  Operator ip Address, first use varchar Deposit 
     */
    private String ip;
    /**
     *  Request parameter record 
     */
    private String params;
    /**
     *  Return result record 
     */
    private String response;
    /**
     *  Operation time 
     */
    private Date createTime;
    /**
     *  Operator id
     */
    private Integer createUser;
    /**
     *  Response time 
     */
    private Date endTime;
    /**
     *  Response result 1 Success 0 Failure 
     */
    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 layer and Service layer are omitted.

5. filter code

1. RequestRecorderGlobalFilter implements GlobalFilter and Order


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  At present, only records request The mode is POST Mode of request 
 * @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();
        // Record only http Request for 
        String scheme = originalRequestUrl.getScheme();
        if ((!"http".equals(scheme) && !"https".equals(scheme))) {
            return chain.filter(exchange);
        }
        // This is what I want to print log-StringBuilder
        StringBuilder logbuilder = new StringBuilder();
        // My own log Entity 
        LogNotes logNotes = new LogNotes();
        //  Return decoding 
        RecorderServerHttpResponseDecorator response = new RecorderServerHttpResponseDecorator(exchange.getResponse(), logNotes, filterService);
        // Request decoding 
        RecorderServerHttpRequestDecorator recorderServerHttpRequestDecorator = new RecorderServerHttpRequestDecorator(exchange.getRequest());
        // Add filtering interception 
        ServerWebExchange ex = exchange.mutate()
                .request(recorderServerHttpRequestDecorator)
                .response(response)
                .build();
        //   Observer mode   Print 1 Next request log
        //  It can be found here in   I configure in the configuration file 
//        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 Original request: \n"), logNotes);
        try {
            filterService.addLog(logNotes);
        } catch (Exception e) {
            logger.error(" Error saving request parameters , e->{}", e.getMessage());
        }
        return result;
    }
    /**
     *  Record the original request logic 
     */
    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("1 Level menu ");
        logNotes.setBmenu("2 Level menu ");
        logNotes.setNotes(" Operation record ");
        logBuffer
                .append(method.toString()).append(' ')
                .append(uri.toString()).append('\n');
        logBuffer.append("------------ Request header ------------\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("------------ None body------------\n");
            } else {
                logBuffer.append("------------body  Length :").append(length).append(" contentType:");
                MediaType contentType = headers.getContentType();
                if (contentType == null) {
                    logBuffer.append("null , do not record body------------\n");
                } else if (!shouldRecordBody(contentType)) {
                    logBuffer.append(contentType.toString()).append(" , do not record 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();
        }
    }
    // Log output return value 
    private Mono<Void> printLog(StringBuilder logBuilder, ServerHttpResponse response) {
        HttpStatus statusCode = response.getStatusCode();
        assert statusCode != null;
        logBuilder.append(" Response: ").append(statusCode.value()).append(" ").append(statusCode.getReasonPhrase()).append('\n');
        HttpHeaders headers = response.getHeaders();
        logBuilder.append("------------ Response header ------------\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() {
        // In GatewayFilter Prior execution 
        return -1;
    }
    private boolean hasBody(HttpMethod method) {
        // Just record this 3 Predicate-like body
//        if (method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH)
        return true;
//        return false;
    }
    // Object that records simple and common text types request Adj. body And response Adj. 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;
        }
        // Don't record for the time being form
        return false;
    }
    //  Get the parameters of the request 
    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());
            // Record the request body of my entity 
            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 inherits ServerHttpRequestDecorator


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);
    }
// Rewrite request Request body 
    @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 inherits ServerHttpResponseDecorator


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;
    }
    /**
     *  Based on netty, I need to show the release here 1 Times dataBuffer, But slice Come out byte There is no need to release it ,
     *  Share with lower layers 1 String buffer pool ,gateway The filter uses the nettyWrite Class , Will happen response Data cannot return complete until it is multiple times. 
     *  In  ServerHttpResponseDecorator  After that, it will release another 1 A 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()];
                //  Data read into array 
                dataBuffer.read(content);
                //  Release memory 
                DataBufferUtils.release(dataBuffer);
                //  Record return value 
                String s = new String(content, Charset.forName("UTF-8"));
                logNotes.setAppendResponse(s);
                try {
                    filterService.updateLog(logNotes);
                } catch (Exception e) {
                    logger.error("Response Error in value modification log record ->{}", 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));
    }
}
 
Note:

Netty service is used at the bottom of gateway filtering return value. When response returns, sometimes the data written is incomplete, so I added an setAppendResponse method to splice in the entity class. Furthermore, the filter of gateway is a chain structure, so it is necessary to define order as the first (-1), and then make an combine with the preset gateway filter.

The dataBuffer structure used in the code, the bottom layer is actually byteBuffer similar to netty, which uses byte array pool and reference counter (refInt).

In order to make jvm in gc garbage collection, to avoid memory leaks, we need to convert bytes in the use of the place, the display of the release once


DataBufferUtils.release(dataBuffer);  

Related articles: