Spring boot webflux 中實現 RequestContextHolder

說明

Spring boot web 中我們可以通過 RequestContextHolder 很方便的獲取 request

ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

// 獲取 request
HttpServletRequest request = requestAttributes.getRequest();

不再需要通過參數傳遞 request。在 Spring webflux 中並沒提供該功能,使得我們在 Aop 或者一些其他的場景中獲取 request 變成了一個奢望???

尋求解決方案

首先我想到的是看看 spring-security 中是否有對於的解決方案,因爲在 spring-security 中我們也是可以通過 SecurityContextHolder 很方便快捷的獲取當前登錄的用戶信息。

找到了 ReactorContextWebFilter,我們來看看 security 中他是怎麼實現的。
https://github.com/spring-pro...

public class ReactorContextWebFilter implements WebFilter {
    private final ServerSecurityContextRepository repository;

    public ReactorContextWebFilter(ServerSecurityContextRepository repository) {
        Assert.notNull(repository, "repository cannot be null");
        this.repository = repository;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(exchange)
            .subscriberContext(c -> c.hasKey(SecurityContext.class) ? c :
                withSecurityContext(c, exchange)
            );
    }

    private Context withSecurityContext(Context mainContext, ServerWebExchange exchange) {
        return mainContext.putAll(this.repository.load(exchange)
            .as(ReactiveSecurityContextHolder::withSecurityContext));
    }
}

源碼裏面我們可以看到 他利用一個 Filter,chain.filter(exchange) 的返回值 Mono 調用了 subscriberContext 方法。
那麼我們就去了解一下這個 reactor.util.context.Context。找到 reactor 官方文檔中的 context 章節:https://projectreactor.io/doc...

大意是:從 Reactor 3.1.0 開始提供了一個高級功能,可以與 ThreadLocal 媲美,應用於 Flux 和 Mono 的上下文工具 Context。更多請大家查閱官方文檔,對英文比較抵觸的朋友可以使用 google 翻譯。

mica 中的實現

mica 中的實現比較簡單,首先是我們的 ReactiveRequestContextFilter

/**
 * ReactiveRequestContextFilter
 *
 * @author L.cm
 */
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class ReactiveRequestContextFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        return chain.filter(exchange)
            .subscriberContext(ctx -> ctx.put(ReactiveRequestContextHolder.CONTEXT_KEY, request));
    }
}

Filter 中直接將 request 存儲到 Context 上下文中。

ReactiveRequestContextHolder 工具:

/**
 * ReactiveRequestContextHolder
 *
 * @author L.cm
 */
public class ReactiveRequestContextHolder {
    static final Class<ServerHttpRequest> CONTEXT_KEY = ServerHttpRequest.class;

    /**
     * Gets the {@code Mono<ServerHttpRequest>} from Reactor {@link Context}
     * @return the {@code Mono<ServerHttpRequest>}
     */
    public static Mono<ServerHttpRequest> getRequest() {
        return Mono.subscriberContext()
            .map(ctx -> ctx.get(CONTEXT_KEY));
    }

}

怎麼使用呢?

mica 中對未知異常處理,從 request 中獲取請求的相關信息

@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Mono<?> handleError(Throwable e) {
    log.error("未知異常", e);
    // 發送:未知異常異常事件
    return ReactiveRequestContextHolder.getRequest()
        .doOnSuccess(r -> publishEvent(r, e))
        .flatMap(r -> Mono.just(R.fail(SystemCode.FAILURE)));
}

private void publishEvent(ServerHttpRequest request, Throwable error) {
    // 具體業務邏輯
}

WebClient 透傳 request 中的 header

此示例來源於開源中國問答中筆者的回覆: 《如何在gateway 中獲取 webflux的 RequestContextHolder》

@GetMapping("/test")
@ResponseBody
public Mono<String> test() {
    WebClient webClient = testClient();
    return webClient.get().uri("").retrieve().bodyToMono(String.class);
}

@Bean
public WebClient testClient() {
    return WebClient.builder()
        .filter(testFilterFunction())
        .baseUrl("https://www.baidu.com")
        .build();
}

private ExchangeFilterFunction testFilterFunction() {
    return (request, next) -> ReactiveRequestContextHolder.getRequest()
        .flatMap(r -> {
            ClientRequest clientRequest = ClientRequest.from(request)
                .headers(headers -> headers.set(HttpHeaders.USER_AGENT, r.getHeaders().getFirst(HttpHeaders.USER_AGENT)))
                .build();
            return next.exchange(clientRequest);
        });
}

上段代碼是透傳 web 中的 request 中的 user_agent 請求頭到 WebClient 中。

開源推薦

關注我們

如夢技術-公衆號.jpg

掃描上面二維碼,更多精彩內容每天推薦!

轉載聲明

如夢技術對此篇文章有最終所有權,轉載請註明出處,參考也請註明,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章