基于RequestResponseBodyMethodProcessor的Trim功能装饰者模式实现


前言

公司内部系统老是有人填表单复制粘贴老是整出前后空格来.

前端项目烂尾, 考虑在服务端增加统一的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 "
}
  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值