文章目錄
- 前言
- 一、實現
- 1.1 @Trim
- 1.2 TrimRequestResponseBodyMethodProcessorDecorator
- 1.3 Configuration
- 二、測試
- 2.1 測試用例
- 2.2 測試結果
- 2.2.1 Test no.1
- 2.2.2 Test no.2
- 2.2.3 Test no.3
- 2.2.4 Test no.4
前言
公司內部系統老是有人填表單復制粘貼老是整出前后空格來.
前端項目爛尾, 考慮在服務端增加統一的trim處理.
一、實現
1.1 @Trim
- 基于hutools的StrUtil.trim(value,mode)方法
- 注解在controller方法參數上時作為開關
/*** Based on {@code cn.hutool.core.util.StrUtil.trim(value, mode)}* <p>* Usage:* <p>* In order to activate the processing, add the annotation on parameters* that were also annotated with {@code @RequestBody}. This will trigger* recursive checking of the fields in the parameters.* <p>* To trim specific fields, annotate on that field.* <p>* If the field is not a String type, the fields to be trimmed in the* object field also have to be annotated with {@code @Trim}.** <pre>* public MODIFIER method({@code @RequestBody @Trim }ParamClass){}** class ParamClass {* {@code @Trim}* private String trimmingField;** {@code @Trim}* private NoneStringField nonStringField;* }** class NoneStringField {* {@code @Trim}* private String nonStringTrimmingField;* }* </pre>** @author hp* @see com.luban.common.base.http.servlet.TrimRequestResponseBodyMethodProcessorDecorator*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.FIELD})
public @interface Trim {Mode value() default Mode.ALL;@Getter@AllArgsConstructorenum Mode implements BaseEnum<Mode, Integer> {/***/END(1, "trimEnd"), ALL(0, "trimAll"), START(-1, "trimStart"),;private final Integer code;private final String name;}
}
1.2 TrimRequestResponseBodyMethodProcessorDecorator
- 裝飾者模式實現, 保留功能同時增強自定義trim處理
- trim目前固定基于hutools, 擴展自定義可以調整為提供一個facade設計注入這個類來提供具體的trim功能
/*** Replace RequestResponseBodyMethodProcessor or add this decorator before it.* <p>* Consider this example of configuring the Decorator** <pre>** {@code @RequiredArgsConstructor}* {@code @Configuration}* public class HandlerMethodArgumentResolverAutoConfiguration {** private final RequestMappingHandlerAdapter requestMappingHandlerAdapter;* private final List<HttpMessageConverter<?>> converters;** {@code @PostConstruct}* public void setRequestExcelArgumentResolver() {* List<HandlerMethodArgumentResolver> argumentResolvers = this.requestMappingHandlerAdapter.getArgumentResolvers();* List<HandlerMethodArgumentResolver> resolverList = new ArrayList<>();* resolverList.add(new TrimRequestResponseBodyMethodProcessorDecorator(new RequestResponseBodyMethodProcessor(converters)));* assert argumentResolvers != null;* resolverList.addAll(argumentResolvers);* this.requestMappingHandlerAdapter.setArgumentResolvers(resolverList);* }* }** </pre>** @author hp* @see RequestMappingHandlerAdapter*/
public class TrimRequestResponseBodyMethodProcessorDecorator implements HandlerMethodArgumentResolver {private final RequestResponseBodyMethodProcessor processor;public TrimRequestResponseBodyMethodProcessorDecorator(@NonNull RequestResponseBodyMethodProcessor processor) {this.processor = processor;}@Overridepublic boolean supportsParameter(@NonNull MethodParameter parameter) {return processor.supportsParameter(parameter);}@Overridepublic Object resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {final Object o = processor.resolveArgument(parameter, mavContainer, webRequest, binderFactory);if (Objects.isNull(o)) {return parameter.isOptional() ? Optional.empty() : null;}// 開關parameter = parameter.nestedIfOptional();if (!parameter.hasParameterAnnotation(Trim.class)) {return o;}// 拿到真實數據對象, 因為原生支持Optional封裝Object object;if (parameter.isOptional()) {final Optional<?> optional = (Optional<?>) o;assert optional.isPresent();object = optional.get();} else {object = o;}// 范型情況, 找到真實的對象Class<?> targetClass;if (parameter.getNestedGenericParameterType() instanceof Class<?> clazz) {targetClass = clazz;} else {ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);targetClass = resolvableType.resolve();}if (Objects.isNull(targetClass)) {return o;}trimObject(targetClass, object);return o;}private static void trimObject(Class<?> targetClass, Object object) {if (Objects.isNull(targetClass) || targetClass == Object.class) {return;}ReflectionUtils.doWithFields(targetClass,field -> {final Trim fieldTrim = field.getAnnotation(Trim.class);assert fieldTrim != null;if (field.getType() == String.class) {ReflectionUtils.makeAccessible(field);final String stringVal = (String) field.get(object);final String trimmedVal = StrUtil.trim(stringVal, fieldTrim.value().getCode());field.set(object, trimmedVal);} else {ReflectionUtils.makeAccessible(field);final Object value = field.get(object);// 從實際對象取, 避免范型問題final Class<?> fieldClass = value.getClass();trimObject(fieldClass, value);}},field -> (AnnotatedElementUtils.hasAnnotation(field, Trim.class)));}
}
1.3 Configuration
- 1.1 僅實現請求參數處理, 響應未實現, 所以配置上僅配置參數處理器即可, 添加到原生處理器之前即可
@RequiredArgsConstructor
@Configuration
public class HandlerMethodArgumentResolverAutoConfiguration {private final RequestMappingHandlerAdapter requestMappingHandlerAdapter;private final List<HttpMessageConverter<?>> converters;@PostConstructpublic void setRequestExcelArgumentResolver() {List<HandlerMethodArgumentResolver> argumentResolvers = this.requestMappingHandlerAdapter.getArgumentResolvers();List<HandlerMethodArgumentResolver> resolverList = new ArrayList<>();resolverList.add(new TrimRequestResponseBodyMethodProcessorDecorator(new RequestResponseBodyMethodProcessor(converters)));assert argumentResolvers != null;resolverList.addAll(argumentResolvers);this.requestMappingHandlerAdapter.setArgumentResolvers(resolverList);}
}
二、測試
2.1 測試用例
- RequestResponseBodyMethodProcessor 翻了下源碼, 支持Optional封裝, 所以用例也測試一下這種場景
@Data
public static class JsonPayload {@Trim(Trim.Mode.START)private String data;
}@Data
public static class JsonPayload2 {@Trimprivate JsonPayload data;
}@PostMapping("/json")
public Returns<String> json(@RequestBody @Trim JsonPayload jsonPayload) {return Returns.success(jsonPayload.data);
}@PostMapping("/json2")
public Returns<String> json2(@RequestBody @Trim Optional<JsonPayload> jsonPayload) {if (jsonPayload.isEmpty()) {return Returns.fail();}return Returns.success(jsonPayload.get().data);
}@PostMapping("/json3")
public Returns<String> json3(@RequestBody @Trim JsonPayload2 jsonPayload) {return Returns.success(jsonPayload.data.data);
}@PostMapping("/json4")
public Returns<String> json4(@RequestBody @Trim Optional<JsonPayload2> jsonPayload) {if (jsonPayload.isEmpty()) {return Returns.fail();}return Returns.success(jsonPayload.get().data.data);
}
2.2 測試結果
2.2.1 Test no.1
# request
POST http://localhost:9999/json
Content-Type: application/json{"data": " 123 "
}
# response
{"code": 200,"message": "操作成功","data": "123 "
}
2.2.2 Test no.2
- Optional包裝
# request
POST http://localhost:9999/json2
Content-Type: application/json{"data": " 123 "
}
# response
{"code": 200,"message": "操作成功","data": "123 "
}
2.2.3 Test no.3
- 嵌套對象
- 外層對象注解屬性trim兩頭, 但是內部string屬性覆蓋為trim開頭
# request
POST http://localhost:9999/json3
Content-Type: application/json{"data": {"data": " 123 "}
}
# response
{"code": 200,"message": "操作成功","data": "123 "
}
2.2.4 Test no.4
- Optional包裝 + 嵌套對象
# request
POST http://localhost:9999/json4
Content-Type: application/json{"data": {"data": " 321 "}
}
# response
{"code": 200,"message": "操作成功","data": "321 "
}