
การสร้าง Custom WebFilter / HandlerStrategies
เวลาอ่าน ≈ 20 นาที — บทความนี้อธิบายตั้งแต่โครงสร้าง Reactive Filter Chain ของ Spring WebFlux ไปจนถึงวิธีปรับแต่ง HandlerStrategies เพื่อควบคุม Codec, ExceptionHandler และอื่น ๆ พร้อมตัวอย่างโค้ด, รูปประกอบ และแนวปฏิบัติดี ๆ สำหรับโปรดักชัน
สารบัญ
- สถาปัตยกรรม WebFlux Processing Flow
- WebFilter พื้นฐาน
- สร้าง Custom WebFilter ขั้นสูง
- กำหนดลำดับและเงื่อนไขการทำงาน
- HandlerStrategies คืออะไร?
- ปรับแต่ง Codec (JSON, Protobuf, CBOR)
- Custom WebExceptionHandler
- ทดสอบด้วย WebTestClient
- Performance & Memory Tips
- Checklist ก่อนขึ้น Production
1. สถาปัตยกรรม WebFlux Processing Flow

เมื่อรับ HTTP request Netty จะส่งต่อให้ ReactorHttpHandlerAdapter
ซึ่งดึง WebHandler
หลัก (โดยปกติคือ DispatcherHandler
). ก่อนถึงตัว Handler จะมี WebFilter Chain ทำงาน sequential ตาม order. ส่วนการสร้าง Response จะใช้ส่วนประกอบภายใต้ HandlerStrategies
เช่น HttpMessageReader/Writer
, ViewResolver
, LocaleContextResolver
ฯลฯ
2. WebFilter พื้นฐาน
@Component public class LoggingWebFilter implements WebFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { long start = System.currentTimeMillis(); return chain.filter(exchange) .doOnSuccess(v -> { long ms = System.currentTimeMillis() - start; log.info("{} {} took {} ms", exchange.getRequest().getMethod(), exchange.getRequest().getPath(), ms); }); } }
WebFilter ต้อง non-blocking เสมอ เพราะรันบน event-loop
3. สร้าง Custom WebFilter ขั้นสูง
3.1 Header Enrichment Filter
public class HeaderEnrichmentFilter implements WebFilter { private final Supplier<String> correlationIdGen; public HeaderEnrichmentFilter(Supplier<String> correlationIdGen) { this.correlationIdGen = correlationIdGen; } @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { String cid = correlationIdGen.get(); ServerHttpRequest mutated = exchange.getRequest() .mutate() .header("X-Correlation-Id", cid) .build(); return chain.filter(exchange.mutate().request(mutated).build()); } }
3.2 Register ผ่าน Bean
@Configuration public class FilterConfig { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) // กำหนดลำดับ public WebFilter correlationFilter() { return new HeaderEnrichmentFilter(UUID::randomUUID); } }
4. กำหนดลำดับและเงื่อนไขการทำงาน
- ใช้
@Order
หรือ implementOrdered
- กรณี Security ใช้
SecurityWebFilterChain
ซึ่งเป็น Filter เองเช่นกัน - เลือกทำงานเฉพาะ Path ได้ด้วย if/else ด้านใน filter ก่อนเรียก chain
5. HandlerStrategies คืออะไร?

HandlerStrategies
เป็น Factory Object ที่ถูกสร้างใน WebHttpHandlerBuilder
. เราสามารถ:
- เพิ่ม/ลด
WebFilter
แบบ Global - ลงทะเบียน
HttpMessageReader/Writer
- กำหนด
WebExceptionHandler
ของตัวเอง
@Bean public HandlerStrategies handlerStrategies(Jackson2JsonDecoder decoder) { return HandlerStrategies.builder() .codecs(cfg -> cfg.defaultCodecs().jackson2JsonDecoder(decoder)) .webFilters(flist -> flist.add(new SecurityHeadersFilter())) .exceptionHandlers(handlers -> handlers.add(new GlobalExceptionHandler())) .build(); }
6. ปรับแต่ง Codec
6.1 ลด Memory ผ่าน Streaming JSON
@Bean public Jackson2JsonDecoder streamingDecoder() { Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(); decoder.getObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); return decoder; }
6.2 เพิ่ม Protobuf
@Bean public HandlerStrategies protobufStrategies() { return HandlerStrategies.builder() .codecs(cfg -> cfg.customCodecs().register(new ProtobufDecoder())) .build(); }
7. Custom WebExceptionHandler
@Component @Order(-2) // ให้ทำงานก่อน ErrorWebExceptionHandler ของ Spring Boot public class GlobalExceptionHandler implements WebExceptionHandler { @Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) { HttpStatus status = (ex instanceof IllegalArgumentException) ? HttpStatus.BAD_REQUEST : HttpStatus.INTERNAL_SERVER_ERROR; ErrorBody body = new ErrorBody(status.value(), ex.getMessage()); return ServerResponse.status(status) .contentType(MediaType.APPLICATION_JSON) .bodyValue(body) .flatMap(resp -> resp.writeTo(exchange, HandlerStrategies.withDefaults())); } }
8. ทดสอบด้วย WebTestClient
@WebFluxTest class FilterTest { @Autowired WebTestClient client; @Test void shouldAddCorrelationHeader() { client.get().uri("/hello") .exchange() .expectHeader().exists("X-Correlation-Id") .expectStatus().isOk(); } }
9. Performance & Memory Tips
- หลีกเลี่ยงการสร้าง
DataBuffer
ใหม่ใน Filter; ใช้DataBufferUtils.retain
- ถ้าต้องแก้ไข Body ใช้
ServerWebExchangeDecorator
+ Caching Buffer แต่ระวัง OOM - บีบอัด Response เปิด
server.compression.enabled=true
- เปิด
BlockHound
ตรวจ blocking call ใน Filter/ExceptionHandler
10. Checklist ก่อน Production
- ทุก Filter & Handler non-blocking 100%
- เขียน Unit/Integration Test ยืนยันลำดับ Filter
- เก็บ Metric
reactor.netty.http.server.requests
และ Error Rate - ใช้
HandlerStrategies.builder()
เดียว เพื่อความสม่ำเสมอ - ตั้งค่า
spring.codec.max-in-memory-size
ให้เหมาะกับ Heap
สรุป
การสร้าง Custom WebFilter ทำให้เราดักจับและปรับเปลี่ยน Request/Response ได้อย่างยืดหยุ่น ขณะที่ HandlerStrategies ช่วยจัดระเบียบ Codec, Filter, และ ExceptionHandler ในจุดเดียว. หากออกแบบดี ๆ คุณจะได้แอป WebFlux ที่ เร็ว, ปลอดภัย, และ ดูแลง่าย พร้อมต่อยอดฟีเจอร์ใหม่ได้ตลอดเวลา