SpringBoot 引數別名實現

Dale 發表於 2021-10-20
Spring

SpringBoot 引數別名實現

首發於 Dale‘s blog

背景與痛點

專案中經常出現一種情況:定了某個引數之後,前端又要求改引數名字,而你又不想因為這個名字而改變程式碼的優雅,於是就需要擁有一個引數對應兩個引數名的能力。
具體來說:業務擁有自定義協議,在java實體中定義的名字是全名,例如:name getter、setter 分別為 getName()setName(String name)
但是,在協議轉發的過程中需要使用簡寫來優化訊息體的大小{"nm":"Dale"},不知道何種原因,前端要求使用nm對應原有的name

思路

使用註解設定引數的別名,將別名與實體的引數名繫結。然後再利用ExtendedServletRequestDataBinder.addBindValues重新將別名對應的值與實體的引數名對應。有些拗口,以背景中的例子為例:接收到的引數為nm,值為Dale。經過繫結之後,將nm的值繫結到name上。

廢話不多,直接上程式碼。

程式碼

ValueFrom

/**
 * 請求引數別名註解
 *
 * @author Dale
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValueFrom {

    /**
     * 引數別名列表
     */
    String[] value();
}

AliasDataBinder

/**
 * 別名資料繫結
 *
 * @author Dale
 */
public class AliasDataBinder extends ExtendedServletRequestDataBinder {

    public AliasDataBinder(Object target, String objectName) {
        super(target, objectName);
    }

    @Override
    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        super.addBindValues(mpvs, request);
        Class<?> targetClass = Objects.requireNonNull(getTarget()).getClass();
        Class<?> targetFatherClass = targetClass.getSuperclass();
        // 利用反射獲取類的欄位
        Field[] fields = targetClass.getDeclaredFields();
        Field[] superFields = targetFatherClass.getDeclaredFields();

        for (Field field : fields) {
            ValueFrom valueFromAnnotation = field.getAnnotation(ValueFrom.class);
            if (mpvs.contains(field.getName()) || valueFromAnnotation == null) {
                continue;
            }
            for (String alias : valueFromAnnotation.value()) {
                if (mpvs.contains(alias)) {
                    mpvs.add(field.getName(), Objects.requireNonNull(mpvs.getPropertyValue(alias)).getValue());
                    break;
                }
            }
        }
        // 將引數繫結到父類上
        for (Field field : superFields) {
            ValueFrom valueFromAnnotation = field.getAnnotation(ValueFrom.class);
            if (mpvs.contains(field.getName()) || valueFromAnnotation == null) {
                continue;
            }
            for (String alias : valueFromAnnotation.value()) {
                if (mpvs.contains(alias)) {
                    mpvs.add(field.getName(), Objects.requireNonNull(mpvs.getPropertyValue(alias)).getValue());
                    break;
                }
            }
        }
    }
}

AliasModelAttributeMethodProcessor

重新注入DataBinder

/**
 * 引數別名繫結processor
 *
 * @author Dale
 */
public class AliasModelAttributeMethodProcessor extends ServletModelAttributeMethodProcessor {

    private ApplicationContext applicationContext;

    public AliasModelAttributeMethodProcessor(boolean annotationNotRequired) {
        super(annotationNotRequired);
    }

    public void setApplicationContext(ApplicationContext applicationContext){
        this.applicationContext = applicationContext;
    }

    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
        // 重新注入 databinder
        AliasDataBinder aliasDataBinder = new AliasDataBinder(binder.getTarget(), binder.getObjectName());
        RequestMappingHandlerAdapter requestMappingHandlerAdapter = applicationContext.getBean(RequestMappingHandlerAdapter.class);
        Objects.requireNonNull(requestMappingHandlerAdapter.getWebBindingInitializer()).initBinder(aliasDataBinder);
        aliasDataBinder.bind(Objects.requireNonNull(request.getNativeRequest(ServletRequest.class)));
    }
}

最後利用WebMvcConfigurer.addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)注入AliasModelAttributeMethodProcessor

WebMvcConfiguration

/**
 * web mvc 配置
 *
 * @author Dale
 */
@Component
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        AliasModelAttributeMethodProcessor aliasModelAttributeMethodProcessor = new AliasModelAttributeMethodProcessor(true);
        aliasModelAttributeMethodProcessor.setApplicationContext(applicationContext);
        // 注入 AliasModelAttributeMethodProcessor
        resolvers.add(aliasModelAttributeMethodProcessor);
        WebMvcConfigurer.super.addArgumentResolvers(resolvers);
    }
}

使用

在定義傳入引數的時候使用註解設定別名

RequestParam

/**
 * 傳送訊息必要引數
 *
 * @author baoxulong
 */
public class RequestParam {
    /**
     * name
     */
    @NotNull(message = "name is required!")
    @ValueFrom(value = "nm")
    private String name;
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
}

NameController

/**
 * name 控制器
 *
 * @author Dale
 */
@RestController
@RequestMapping("name")
public class NameController {

    @PostMapping("set")
    public JsonResult set(@Valid RequestParam requestParam) {
        // todo::
        return JsonResult.success();
    }
    
}

總結

以上是 SpringBoot 設定引數別名的方法程式碼實記,深度不高,找時間再深挖。