文章目录
前言
公司内部系统老是有人填表单复制粘贴老是整出前后空格来.
前端项目烂尾, 考虑在服务端增加统一的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
@AllArgsConstructor
enum 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;
}
@Override
public boolean supportsParameter(@NonNull MethodParameter parameter) {
return processor.supportsParameter(parameter);
}
@Override
public 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;
@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);
}
}
二、测试
2.1 测试用例
- RequestResponseBodyMethodProcessor 翻了下源码, 支持Optional封装, 所以用例也测试一下这种场景
@Data
public static class JsonPayload {
@Trim(Trim.Mode.START)
private String data;
}
@Data
public static class JsonPayload2 {
@Trim
private 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 "
}