1. 程式人生 > >SpringMVC物件繫結時自定義名稱對應關係

SpringMVC物件繫結時自定義名稱對應關係

例行推廣一下我的部落格,喜歡這篇文章的朋友可以看我的部落格http://zwgeek.com

這個需求來源自一個Post的Controller的請求含有太多的引數,於是想把所有的引數封裝到物件中,然後Controller的方法接收一個物件型別的引數,這樣後期擴充套件修改都比較方便,不需要動到方法簽名。

有一句俗話說得好,需求是第一生產力,上面的這個需求就催生了這篇文章的一系列調研。

首先,這個需求SpringMVC本身是支援的,你把一個物件放在Controller方法的引數裡,SpringMVC本身的支援就能把request中和這個物件屬性同名的引數繫結到物件屬性上,比如下面這樣:

@RequestMapping
(value = "/test", method = RequestMethod.GET) public void test(Test test) { LOG.debug(test.toString()); }

Test中是這樣定義的

public class Test {

    private String test1;

    private String test2;

    private String test3;
}

由於我使用了lombok,所以沒有Getter和Setter方法,大家看demo的時候可以注意下。這個時候我們訪問/test?test1=1&test2=2&test3=3,SpringMVC會自動把同名的屬性和Request中的引數繫結在一起。

這裡寫圖片描述

到這裡貌似可以結題了,開玩笑,哪有這麼簡單。大家想這樣一種情況,在JAVA中我們用的是駝峰命名法(比如:testName),而在前端JS中我們大多用的是蛇形命名法(比如:test_name)。當然,我們可以要求前端在請求介面的時候用駝峰命名法,但是問題不是這樣逃避的。另外,如果我前端介面引數的名字和物件裡面屬性的名字不一樣怎麼辦呢,比如前端接口裡引數叫person,但是我物件裡的屬性叫people,當然這種情況比較少,但是也不排除在各種複雜的需求中會出現這種情況,所以我們這篇文章的目的就是做到自定義的引數名和屬性名對映,想讓哪個請求引數對應哪個屬性都可以,想點哪裡點哪裡就是這個意思。

這篇文章只講解決方案,我會在下一篇文中講一下SpringMVC資料繫結的實現原理。

雖然不細說原理,但是有幾個概念還是要提前說一下的,使用SpringMVC時,所有的請求都是最先經過DispatcherServlet的,然後由DispatcherServlet選擇合適的HandlerMapping和HandlerAdapter來處理請求,HandlerMapping的作用就是找到請求所對應的方法,而HandlerAdapter則來處理和請求相關的的各種事情。我們這裡要講的請求引數繫結也是HandlerAdapter來做的。大概就知道這些吧,我們需要寫一個自定義的請求引數處理器,然後把這個處理器放到HandlerAdapter中,這樣我們的處理器就可以被拿來處理請求了。

首先第一步,我們先來做一個引數處理器SnakeToCamelModelAttributeMethodProcessor

public class SnakeToCamelModelAttributeMethodProcessor extends ServletModelAttributeMethodProcessor implements ApplicationContextAware{

    ApplicationContext applicationContext;

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

    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
        SnakeToCamelRequestDataBinder camelBinder = new SnakeToCamelRequestDataBinder(binder.getTarget(), binder.getObjectName());
        RequestMappingHandlerAdapter requestMappingHandlerAdapter = applicationContext.getBean(RequestMappingHandlerAdapter.class);
        requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(camelBinder, request);
        camelBinder.bind(request.getNativeRequest(ServletRequest.class));
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

這個處理器要繼承ServletModelAttributeMethodProcessor,來看下繼承關係
這裡寫圖片描述

看最上面實現的是HandlerMethodArgumentResolver介面,這個介面代表這個類是用來處理請求引數的,有兩個方法

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter var1);

    Object resolveArgument(MethodParameter var1, ModelAndViewContainer var2, NativeWebRequest var3, WebDataBinderFactory var4) throws Exception;
}

supportsParameter返回是否支援這種引數,resolveArgument是具體處理引數的方法。ServletModelAttributeMethodProcessor是處理複雜物件的,也就是除了int,char等等簡單物件之外自定義的複雜物件,比如上文中我們提到的Test。

我們自定義的處理器也是處理複雜物件,只是擴充套件了可以處理名稱對映,所以繼承這個ServletModelAttributeMethodProcessor即可。好了,處理器寫好了,那麼接下來怎麼做呢,重寫父類的bindRequestParameters方法,這個方法就是繫結資料物件的時候呼叫的方法。

在這個方法中,我們新建了一個自定義的DataBinder-SnakeToCamelRequestDataBinder,然後用HandlerAdapter初始化了這個DataBinder,最後調了DataBinder的bind方法。DataBinder顧名思義就是實際去把請求引數和物件繫結的類,這個自定義的DataBinder怎麼寫呢,如下:

public class SnakeToCamelRequestDataBinder extends ExtendedServletRequestDataBinder {

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

    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        super.addBindValues(mpvs, request);

        //處理JsonProperty註釋的物件
        Class<?> targetClass = getTarget().getClass();
        Field[] fields = targetClass.getDeclaredFields();
        for (Field field : fields) {
            JsonProperty jsonPropertyAnnotation = field.getAnnotation(JsonProperty.class);
            if (jsonPropertyAnnotation != null && mpvs.contains(jsonPropertyAnnotation.value())) {
                if (!mpvs.contains(field.getName())) {
                    mpvs.add(field.getName(), mpvs.getPropertyValue(jsonPropertyAnnotation.value()).getValue());
                }
            }
        }

        List<PropertyValue> covertValues = new ArrayList<PropertyValue>();
        for (PropertyValue propertyValue : mpvs.getPropertyValueList()) {
            if(propertyValue.getName().contains("_")) {
                String camelName = SnakeToCamelRequestParameterUtil.convertSnakeToCamel(propertyValue.getName());
                if (!mpvs.contains(camelName)) {
                    covertValues.add(new PropertyValue(camelName, propertyValue.getValue()));
                }
            }
        }
        mpvs.getPropertyValueList().addAll(covertValues);
    }
}

這個自定義的DataBinder繼承自ExtendedServletRequestDataBinder,可擴充套件的DataBinder,用來給子類複寫的方法是addBindValues,有兩個引數,一個是MutablePropertyValues型別的,這裡面存的就是請求引數的key-value對,還有一個引數是request物件本身。request物件這裡用不到,我們用的就是這個MutablePropertyValues型別的mpvs。

其實處理的原理很簡單,SpringMVC在做完這一步引數繫結之後就會去通過反射呼叫Controller中的方法了,呼叫Controller方法的時候要給引數賦值,賦值的時候就是從這個mpvs裡面把對應引數name的value取出來。舉個例子,我們的樣例Controller中的物件時Test,Test有個屬性是Test1,那麼在給Test1賦值的時候就會從這個mpvs中去取key為Test1所對應的value。可是你想想,前端請求的引數是test_1這樣的,所以這個mpvs中只有一個key為test_1的值,那自然就會報錯。知道了這種處理方法,就很簡單了,我們在這個mpvs中再加一個key為test1,value和test_1的value一樣的物件就可以了。

再擴充套件一點,說到自定義Name,比如test_1對應屬性為test2這樣,我們用一個有value的註釋,比如這裡用到的JsonProperty,在test2上加上註釋@JsonProperty(“test_1”),這裡處理的時候會先把這個註釋的值取出來,從mpvs裡面查,如果有這個key,那麼就把value取出來再加進去一個key為field name的map就可以了。上面SnakeToCamelRequestDataBinder的處理方法大概就是這樣了。

然後這裡我們抽出了一個工具類,用來處理蛇形string到駝峰string的轉換。

public class SnakeToCamelRequestParameterUtil {
    public static String convertSnakeToCamel(String snake) {

        if (snake == null) {
            return null;
        }

        if (snake.indexOf("_") < 0) {
            return snake;
        }

        String result = "";

        String[] split = StringUtils.split(snake, "_");
        int index = 0;
        for (String s : split) {
            if (index == 0) {
                result += s.toLowerCase();
            } else {
                result += capitalize(s);
            }
            index++;
        }

        return result;
    }

    private static String capitalize(String s) {

        if (s == null) {
            return null;
        }

        if (s.length() == 1) {
            return s.toUpperCase();
        }

        return s.substring(0, 1).toUpperCase() + s.substring(1);
    }
}

做完了上面這些,應該說處理過程就搞定了,還差最後一步,需要把我們自定義的處理器SnakeToCamelModelAttributeMethodProcessor加到系統的HandlerAdapter中去。方法有很多,如果你不知道HandlerAdapter是什麼東西,那八成你用的是系統預設的HandlerAdapter。加起來也很簡單。如果你用了<mvc:annotation-driven>元素,可以用下面這個方法。

<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="package.name.SnakeToCamelModelAttributeMethodProcessor">
            <constructor-arg name="annotationNotRequired" value="true"/>
        </bean>
    </mvc:argument-resolvers>
</mvc:annotation-driven> 

如果你用的是JAVA程式碼配置,可以用

@Configuration
public class WebContextConfiguration extends WebMvcConfigurationSupport {
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(processor());
    }

    @Bean
    protected SnakeToCamelModelAttributeMethodProcessor processor() {
        return new SnakeToCamelModelAttributeMethodProcessor(true);
    }
} 

像我這邊,因為專案需要自定義了HandlerAdapter。

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        ...
    </bean>

所以我寫了一個註冊器,用來把處理器註冊進HandlerAdapter,程式碼如下。

public class SnakeToCamelProcessorRegistry implements ApplicationContextAware, BeanFactoryPostProcessor {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        RequestMappingHandlerAdapter requestMappingHandlerAdapter = applicationContext.getBean(RequestMappingHandlerAdapter.class);

        List<HandlerMethodArgumentResolver> resolvers = requestMappingHandlerAdapter.getArgumentResolvers().getResolvers();


        List<HandlerMethodArgumentResolver> newResolvers = new ArrayList<HandlerMethodArgumentResolver>();

        for (HandlerMethodArgumentResolver resolver : resolvers) {
            newResolvers.add(resolver);
        }
        SnakeToCamelModelAttributeMethodProcessor processor = new SnakeToCamelModelAttributeMethodProcessor(true);
        processor.setApplicationContext(applicationContext);
        newResolvers.add(0, processor);
        requestMappingHandlerAdapter.setArgumentResolvers(Collections.unmodifiableList(newResolvers));
    }
}

看起來可能邏輯比較複雜,為什麼要做這一堆事情呢,話要從HandlerAdapter裡系統自帶的處理器說起。我這邊系統預設帶了24個處理器,其中有兩個ServletModelAttributeMethodProcessor,也就是我們自定義處理器繼承的系統處理器。SpringMVC處理請求引數是輪詢每一個處理器,看是否支援,也就是supportsParameter方法, 如果返回true,就交給你出來,並不會問下面的處理器。這就導致瞭如果我們簡單的把我們的自定義處理器加到HandlerAdapter的Resolver列中是不行的,需要加到第一個去。

然後ServletModelAttributeMethodProcessor的構造器有一個引數是true,代表什麼意思呢,看這句程式碼

public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(ModelAttribute.class)?true:(this.annotationNotRequired?!BeanUtils.isSimpleProperty(parameter.getParameterType()):false);
    }

ServletModelAttributeMethodProcessor是否支援某種型別的引數,是這樣判斷的。首先,物件是否有ModelAttribute註解,如果有,則處理,如果沒有,則判斷annotationNotRequired,是否不需要註釋,如果true,再判斷物件是否簡單物件。我們的Test物件是沒有註釋的,所以我們就需要傳參為true,表示不一定需要註解。

以上就是所有的配置過程,通過這一系列配置,我們就可以自定義前端請求引數和物件屬性名稱的對映關係了,通過JsonProperty註解,如果沒有註解,會自動轉換蛇形命名到駝峰命名。下面是效果,Demo的Controller依然是這個:

@RequestMapping(value = "/test", method = RequestMethod.GET)
    @ResponseBody
    public void test(Test test) {
        LOG.debug(test.toString());
    }

Test物件這樣寫

public class Test {

    @JsonProperty("test_1")
    private String test1;

    private String test2;

    @JsonProperty("test_3")
    private String test99;
}

這樣前端請求的引數中的test_1和test1都會繫結到test1,test_2和test2都會繫結到test2,test_3,test99和test_99都會繫結到test99上。我們試一下這個請求

/test?test_1=1&test_2=2&test_3=3

Log輸出如下

這裡寫圖片描述

下一篇文章準備詳細的講一下SpringMVC處理請求最後對映到一個Controller方法上的過程。

例行推廣一下我的部落格,喜歡這篇文章的朋友可以看我的部落格http://zwgeek.com

相關推薦

SpringMVC物件定義名稱對應關係

例行推廣一下我的部落格,喜歡這篇文章的朋友可以看我的部落格http://zwgeek.com 這個需求來源自一個Post的Controller的請求含有太多的引數,於是想把所有的引數封裝到物件中,然後Controller的方法接收一個物件型別的引數,這樣後期擴

SpringMVC學習(一)引數定義轉換器,處理請求亂碼

一、註解對映器和介面卡 1,元件掃描器 使用元件掃描器省去在spring容器配置每個controller類的繁瑣。 <!--開啟註解掃描 --> <context:component-scan base-package="com.

Angular6實現HTML定義屬性的值以及CSS中background屬性的資料

今天用Angular6在整合網上一個程式碼的時候,他的程式碼的一個HTML標籤有幾個自定義標籤,然後我以為轉換到Angular後和不是自定義標籤一樣直接加[]就可以了,但是一直報錯。 這裡顯示沒有這個屬性  解決方案:去除[]並且加上attr.就可以了

SpringMVC 資料,表單輸入值與實體資料型別一一對應的問題

SpringMVC 資料繫結的一個小小的錯誤,竟浪費了不少時間,趕緊記下來,免得重蹈覆轍。 Model public class Student{ private String name;

androidStudio 定義控制元件在XML使用xmlns定義名稱空間時報錯?

在androidstudio中自定義控制元件時在XML中使用自定義屬性的名稱空間 現在這樣使用會報錯 xmlns:example="http://schemas.android.com/apk/re

springMVC筆記系列(11)——使用 POJO 物件請求引數值

Spring MVC 會按請求引數名和 POJO 屬性名進行自動匹配,自動為該物件填充屬性值。支援級聯屬性。如:dept.deptId、dept.address.tel 等 說的通俗點就是,平時我們想將請求頁面的表單資料接收並封裝成特定物件的時候,少不了做的是

SpringMVC(十二)定義異常處理器 HandlerExceptionResolver(接口)

pin org ota admin pack property framework ase exception 自定義異常處理器和系統異常處理器的提升版可以實現相同的功能,但是使用的方法不同,自定義異常處理器可以不用在配置文件中配置name多東西,只需要一個異常處理器就可以

SSM-SpringMVC-24:SpringMVC異常高級之定義異常

BE request input suffix super() internal except simple res ------------吾亦無他,唯手熟爾,謙卑若愚,好學若饑------------- 自定義異常,大家都會,對吧,無非就是繼承異常類等操作,

SSM-SpringMVC-26:SpringMVC異常駭級之定義異常註解版

常對象 esp 方法 ror ref base super 定義 type ------------吾亦無他,唯手熟爾,謙卑若愚,好學若饑------------- 註解的方法實現異常解析,話不多說,直接搞起,和以前一樣的習慣,和上篇博客一樣的代碼放後面,

android打包生成apk定義文件名版本號。定義項目字段等等

field col each deb 自定義 文件名 all != null 早期的AS2.0版本左右中這樣配置: app---->build.gradle中設置 applicationVariants.all { variant ->

SpringMVC_第三章(SpringMVC資料

1. 什麼是引數繫結 引數繫結,簡單來說就是客戶端傳送請求,而請求中包含一些資料,那麼這些資料怎麼到達 Controller ?這在實際專案開發中也是用到的最多的,那麼 SpringMVC 的引數繫結是怎麼實現的呢?下面我們來詳細的講解。 在springMVC之前 首先在使用spring

對同一個物件多個響應事件並都執行,和此例子的相容程式碼

要點: 1.因為 onclick=" "  新增的元素響應事件,先新增的事件,會被後來新增的事件層疊掉,只能執行最後一個響應的事件 所以要用到事件監聽addElementLitener()來繫結多個處理函式,而因為相容性的問題需要相容程式碼。 2.在IE8中,addE

VUE.JS 使用axios資料請求資料 報錯 TypeError: Cannot set property 'xxxx' of undefined 的解決辦法

正常情況下在data裡面都有做了定義 在函式裡面進行賦值 這時候你執行時會發現,資料可以請求到,但是會報錯 TypeError: Cannot set property 'listgroup' of undefined  主要原因是: 在 then的內部不能使用Vue的例項

使用DRF視圖集定義action方法

request 代碼 ont .py 設置 spa esp ews 沒有 在我們用DRF視圖集完成了查找全部部門,創建一個新的部門,查找一個部門,修改一個部門,刪除一個部門的功能後,views.py的代碼是這樣子的: class DepartmentViewSet(Mod

讓xamarin的Entry,支援Nullable型別

xamarin.forms預設情況下,如果屬性是double?型別,繫結到Entry上,是無法實現雙向繫結的, 可以自定義Converter實現雙向繫結 public class NullableConverter : IValueConverter { public

SpringMVC 引數相關注解

@RequestParams 作用:把請求中指定名稱的引數給控制器中的形參賦值。 屬性: // <a href="account/save3.do?id=100&username=jack"> 儲存 2</a>

springboot controller物件屬性轉換:定義json訊息處理器

背景 我們後端寫介面的時候可能會碰到屬性欄位轉換的情況,比如user_name轉成userName,這個時候手動寫get set肯定很不方便,這個時候註解神器就可以用了,常用的有兩種JSONField與JsonProperty。 具體使用 JSONField與JsonProp

SpringMVC 自動資料

第一種:繁重操作解決方式 不在Controller裡面寫InitBinder方法,直接在實體類裡面將DATE型別的欄位上加註解。 /** * 債務履行起始時間 */ @DateTimeFormat(pattern = "yyyy-MM-dd")

SpringMVC Hibernate validator使用以及定義校驗器註解

Hibernate validator使用以及自定義校驗器註解 Hibernate Validator常用註解 1.建立自定義校驗器 import javax.validation.Constraint; import javax.validation.Payloa

TP5.1定義名稱空間

author:咔咔 wechat:fangkangfk 由於步驟比較多,在下一篇文章用命令在data名稱空間建立檔案,本片文章只是建立了data的名稱空間 看下圖會發現,命令列建立的檔案,還是會到app下去。所以下片文章處理 從下圖我們可以看出,facad