การสร้าง Custom WebFilter / HandlerStrategies

Sharing is caring!

ภาพหน้าปก: นักพัฒนากำลังประกอบฟิลเตอร์และ HandlerStrategies ให้ WebFlux

การสร้าง Custom WebFilter / HandlerStrategies

เวลาอ่าน ≈ 20 นาที — บทความนี้อธิบายตั้งแต่โครงสร้าง Reactive Filter Chain ของ Spring WebFlux ไปจนถึงวิธีปรับแต่ง HandlerStrategies เพื่อควบคุม Codec, ExceptionHandler และอื่น ๆ พร้อมตัวอย่างโค้ด, รูปประกอบ และแนวปฏิบัติดี ๆ สำหรับโปรดักชัน


สารบัญ

  1. สถาปัตยกรรม WebFlux Processing Flow
  2. WebFilter พื้นฐาน
  3. สร้าง Custom WebFilter ขั้นสูง
  4. กำหนดลำดับและเงื่อนไขการทำงาน
  5. HandlerStrategies คืออะไร?
  6. ปรับแต่ง Codec (JSON, Protobuf, CBOR)
  7. Custom WebExceptionHandler
  8. ทดสอบด้วย WebTestClient
  9. Performance & Memory Tips
  10. Checklist ก่อนขึ้น Production

1. สถาปัตยกรรม WebFlux Processing Flow

ภาพที่ 1 : Request → WebFilter Chain → WebHandler → HandlerStrategies → Response

เมื่อรับ 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 หรือ implement Ordered
  • กรณี Security ใช้ SecurityWebFilterChain ซึ่งเป็น Filter เองเช่นกัน
  • เลือกทำงานเฉพาะ Path ได้ด้วย if/else ด้านใน filter ก่อนเรียก chain

5. HandlerStrategies คืออะไร?

ภาพที่ 2 : HandlerStrategies รวม Codec, WebFilter, ExceptionHandler ฯลฯ

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

  1. ทุก Filter & Handler non-blocking 100%
  2. เขียน Unit/Integration Test ยืนยันลำดับ Filter
  3. เก็บ Metric reactor.netty.http.server.requests และ Error Rate
  4. ใช้ HandlerStrategies.builder() เดียว เพื่อความสม่ำเสมอ
  5. ตั้งค่า spring.codec.max-in-memory-size ให้เหมาะกับ Heap

สรุป

การสร้าง Custom WebFilter ทำให้เราดักจับและปรับเปลี่ยน Request/Response ได้อย่างยืดหยุ่น ขณะที่ HandlerStrategies ช่วยจัดระเบียบ Codec, Filter, และ ExceptionHandler ในจุดเดียว. หากออกแบบดี ๆ คุณจะได้แอป WebFlux ที่ เร็ว, ปลอดภัย, และ ดูแลง่าย พร้อมต่อยอดฟีเจอร์ใหม่ได้ตลอดเวลา

Leave a Reply

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *