SpringMVC源碼總結(八)類型轉換PropertyEditor的背後

PropertyEditor是Spring最初採用的轉換策略。將會轉移到Converter上。本文章主要對@InitBinder註解背後代碼層面的運行過程做介紹。所以最好先熟悉它的用法然後來看通代碼流程。 


先看實例,controller代碼如下:
 
Java代碼  收藏代碼
  1. @Controller  
  2. public class FormAction{  
  3.       
  4. // 這樣的方法裏,一般是用來註冊一些PropertyEditor  
  5.     @InitBinder    
  6.     public void initBinder(WebDataBinder binder) throws Exception {    
  7.         DateFormat df = new SimpleDateFormat("yyyy---MM---dd HH:mm:ss");    
  8.         CustomDateEditor dateEditor = new CustomDateEditor(df, true);    
  9.         binder.registerCustomEditor(Date.class, dateEditor);        
  10.     }     
  11.       
  12.       
  13.     @RequestMapping(value="/test/json",method=RequestMethod.GET)  
  14.     @ResponseBody  
  15.     public Map<String,Object> getFormData(Date date){  
  16.         Map<String,Object> map=new HashMap<String,Object>();  
  17.         map.put("name","lg");  
  18.         map.put("age",23);  
  19.         map.put("date",new Date());  
  20.         return map;  
  21.     }  
  22. }  

xml文件僅僅開啓mvc:ananotation-driven: 
Java代碼  收藏代碼
  1. <mvc:annotation-driven />    

然後訪問  http://localhost:8080/test/json?date=2014---08---3 03:34:23,便看到成功的獲取到了數據。接下來源代碼代碼分析這一過程: 

由於使用了@RequestMapping所以會選擇RequestMappingHandlerAdapter來調度執行相應的方法,如下:
 
Java代碼  收藏代碼
  1. /** 
  2.      * Invoke the {@link RequestMapping} handler method preparing a {@link ModelAndView} 
  3.      * if view resolution is required. 
  4.      */  
  5.     private ModelAndView invokeHandleMethod(HttpServletRequest request,  
  6.             HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {  
  7.   
  8.         ServletWebRequest webRequest = new ServletWebRequest(request, response);  
  9. //我們關注的重點重點重點重點重點重點重點重點  
  10.         WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);  
  11.         ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);  
  12.         ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);  
  13.   
  14.         ModelAndViewContainer mavContainer = new ModelAndViewContainer();  
  15.         mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));  
  16.         modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);  
  17.         mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);  
  18.   
  19.         AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);  
  20.         asyncWebRequest.setTimeout(this.asyncRequestTimeout);  
  21.   
  22.         final WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);  
  23.         asyncManager.setTaskExecutor(this.taskExecutor);  
  24.         asyncManager.setAsyncWebRequest(asyncWebRequest);  
  25.         asyncManager.registerCallableInterceptors(this.callableInterceptors);  
  26.         asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);  
  27.   
  28.         if (asyncManager.hasConcurrentResult()) {  
  29.             Object result = asyncManager.getConcurrentResult();  
  30.             mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];  
  31.             asyncManager.clearConcurrentResult();  
  32.   
  33.             if (logger.isDebugEnabled()) {  
  34.                 logger.debug("Found concurrent result value [" + result + "]");  
  35.             }  
  36.             requestMappingMethod = requestMappingMethod.wrapConcurrentResult(result);  
  37.         }  
  38.   
  39.         requestMappingMethod.invokeAndHandle(webRequest, mavContainer);  
  40.   
  41.         if (asyncManager.isConcurrentHandlingStarted()) {  
  42.             return null;  
  43.         }  
  44.   
  45.         return getModelAndView(mavContainer, modelFactory, webRequest);  
  46.     }  

這裏面就是整個執行過程。首先綁定請求參數到方法的參數上,然後執行方法,接下來根據方法返回的類型來選擇合適的HandlerMethodReturnValueHandler來進行處理,最後要麼走view路線,要麼直接寫入response的body中返回。 

我們此時關注的重點是:如何綁定請求參數到方法的參數上的呢? 
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); 
針對每次對該handlerMethod請求產生一個綁定工廠,由這個工廠來完成數據的綁定。 
這裏的handlerMethod包含了 controller對象FormAction和、test/json映射到的方法即getFormData。 
然後詳細看下getDataBinderFactory的實現:
 
Java代碼  收藏代碼
  1. private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {  
  2. //這裏的handlerType便是controller的類型FormAction  
  3.         Class<?> handlerType = handlerMethod.getBeanType();  
  4.         Set<Method> methods = this.initBinderCache.get(handlerType);  
  5.         if (methods == null) {  
  6. //關注點1:找出FormAction類的所有的含有@InitBinder的方法(方法的返回類型必須爲void),找到後同時緩存起來  
  7.             methods = HandlerMethodSelector.selectMethods(handlerType, INIT_BINDER_METHODS);  
  8.             this.initBinderCache.put(handlerType, methods);  
  9.         }  
  10.         List<InvocableHandlerMethod> initBinderMethods = new ArrayList<InvocableHandlerMethod>();  
  11.         // Global methods first  
  12. //關注點2:再尋找出全局的初始化Binder的方法  
  13.         for (Entry<ControllerAdviceBean, Set<Method>> entry : this.initBinderAdviceCache .entrySet()) {  
  14.             if (entry.getKey().isApplicableToBeanType(handlerType)) {  
  15.                 Object bean = entry.getKey().resolveBean();  
  16.                 for (Method method : entry.getValue()) {  
  17.                     initBinderMethods.add(createInitBinderMethod(bean, method));  
  18.                 }  
  19.             }  
  20.         }  
  21.         for (Method method : methods) {  
  22.             Object bean = handlerMethod.getBean();  
  23.             initBinderMethods.add(createInitBinderMethod(bean, method));  
  24.         }  
  25. //關注點3:找到了所有的與該handlerMethod有關的初始化binder的方法,保存起來  
  26.         return createDataBinderFactory(initBinderMethods);  
  27.     }  

上面稍微做了些註釋,然後看下詳細的內容: 
關注點1:就是使用過濾,過濾類爲:INIT_BINDER_METHODS,如下
 
Java代碼  收藏代碼
  1. /** 
  2.      * MethodFilter that matches {@link InitBinder @InitBinder} methods. 
  3.      */  
  4.     public static final MethodFilter INIT_BINDER_METHODS = new MethodFilter() {  
  5.   
  6.         @Override  
  7.         public boolean matches(Method method) {  
  8.             return AnnotationUtils.findAnnotation(method, InitBinder.class) != null;  
  9.         }  
  10.     };  

這個過濾類就是在handlerType即FormAction中過濾那些含有@InitBinder註解的方法。找到了之後就緩存起來,供下次使用。key爲:handlerType,value爲找到的方法。存至initBinderCache中。 

關注點2:從initBinderAdviceCache中獲取所有支持這個handlerType的method。這一塊有待繼續研究,這個initBinderAdviceCache是如何初始化來的等等。針對目前的工程來說,initBinderAdviceCache是爲空的。 

關注點3:遍歷所有找到的和handlerType有關的method,然後封裝成InvocableHandlerMethod,如下:
 
Java代碼  收藏代碼
  1. for (Method method : methods) {  
  2.             Object bean = handlerMethod.getBean();  
  3.             initBinderMethods.add(createInitBinderMethod(bean, method));  
  4.         }  

Java代碼  收藏代碼
  1. private InvocableHandlerMethod createInitBinderMethod(Object bean, Method method) {  
  2.         InvocableHandlerMethod binderMethod = new InvocableHandlerMethod(bean, method);  
  3.         binderMethod.setHandlerMethodArgumentResolvers(this.initBinderArgumentResolvers);  
  4.         binderMethod.setDataBinderFactory(new DefaultDataBinderFactory(this.webBindingInitializer));  
  5.         binderMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);  
  6.         return binderMethod;  
  7.     }  

在封裝的過程中,同時設置一些RequestMappingHandlerAdapter的一些參數進去initBinderArgumentResolvers、webBindingInitializer、parameterNameDiscoverer。 
封裝完所有的方法後,創建出最終的WebDataBinderFactory。如下:
 
Java代碼  收藏代碼
  1. protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)  
  2.             throws Exception {  
  3.   
  4.         return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());  
  5.     }  

getWebBindingInitializer()也是RequestMappingHandlerAdapter的webBindingInitializer參數。 

至此綁定數據的工廠完成了,包含了這個handlerType的所有的PropertyEditor。這是準備工作,然後就是等待執行這個我們自己的方法getFormData執行時來完成參數的綁定過程。 

綁定參數過程即getFormData的執行過程如下:
 
Java代碼  收藏代碼
  1. ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);  
  2. 略  
  3. requestMappingMethod.invokeAndHandle(webRequest, mavContainer);  

其中的requestMappingMethod經過了進一步的包裝,已經包含剛纔已經創建的綁定工廠。 
執行過程如下:
 
Java代碼  收藏代碼
  1. public final Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,  
  2.             Object... providedArgs) throws Exception {  
  3.   
  4.         Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);  
  5.         if (logger.isTraceEnabled()) {  
  6.             StringBuilder sb = new StringBuilder("Invoking [");  
  7.             sb.append(getBeanType().getSimpleName()).append(".");  
  8.             sb.append(getMethod().getName()).append("] method with arguments ");  
  9.             sb.append(Arrays.asList(args));  
  10.             logger.trace(sb.toString());  
  11.         }  
  12.         Object returnValue = invoke(args);  
  13.         if (logger.isTraceEnabled()) {  
  14.             logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");  
  15.         }  
  16.         return returnValue;  
  17.     }  

分兩大步,綁定參數和執行方法體。最重要的就是如何來綁定參數呢? 
Java代碼  收藏代碼
  1. private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,  
  2.             Object... providedArgs) throws Exception {  
  3.   
  4.         MethodParameter[] parameters = getMethodParameters();  
  5.         Object[] args = new Object[parameters.length];  
  6.         for (int i = 0; i < parameters.length; i++) {  
  7.             MethodParameter parameter = parameters[i];  
  8.             parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);  
  9.             GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());  
  10.             args[i] = resolveProvidedArgument(parameter, providedArgs);  
  11.             if (args[i] != null) {  
  12.                 continue;  
  13.             }  
  14.             if (this.argumentResolvers.supportsParameter(parameter)) {  
  15.                 try {  
  16.                     args[i] = this.argumentResolvers.resolveArgument(  
  17.                             parameter, mavContainer, request, this.dataBinderFactory);  
  18.                     continue;  
  19.                 }  
  20.                 catch (Exception ex) {  
  21.                     if (logger.isTraceEnabled()) {  
  22.                         logger.trace(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);  
  23.                     }  
  24.                     throw ex;  
  25.                 }  
  26.             }  
  27.             if (args[i] == null) {  
  28.                 String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);  
  29.                 throw new IllegalStateException(msg);  
  30.             }  
  31.         }  
  32.         return args;  
  33.     }  

綁定參數又引出來另一個重要名詞:HandlerMethodArgumentResolver。args[i] = this.argumentResolvers.resolveArgument( 
parameter, mavContainer, request, this.dataBinderFactory);的具體內容如下
: 
Java代碼  收藏代碼
  1. /** 
  2.      * Iterate over registered {@link HandlerMethodArgumentResolver}s and invoke the one that supports it. 
  3.      * @exception IllegalStateException if no suitable {@link HandlerMethodArgumentResolver} is found. 
  4.      */  
  5.     @Override  
  6.     public Object resolveArgument(  
  7.             MethodParameter parameter, ModelAndViewContainer mavContainer,  
  8.             NativeWebRequest webRequest, WebDataBinderFactory binderFactory)  
  9.             throws Exception {  
  10.   
  11.         HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);  
  12.         Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");  
  13.         return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);  
  14.     }  

遍歷所有已註冊的HandlerMethodArgumentResolver,然後找出一個適合的來進行參數綁定,對於本工程來說,getFormData(Date date)的參數date默認是request params級別的,所以使用RequestParamMethodArgumentResolver來處理這一過程。處理過程如下: 
Java代碼  收藏代碼
  1. @Override  
  2.     public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,  
  3.             NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {  
  4.   
  5.         Class<?> paramType = parameter.getParameterType();  
  6.         NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);  
  7.   
  8.         Object arg = resolveName(namedValueInfo.name, parameter, webRequest);  
  9.         if (arg == null) {  
  10.             if (namedValueInfo.defaultValue != null) {  
  11.                 arg = resolveDefaultValue(namedValueInfo.defaultValue);  
  12.             }  
  13.             else if (namedValueInfo.required) {  
  14.                 handleMissingValue(namedValueInfo.name, parameter);  
  15.             }  
  16.             arg = handleNullValue(namedValueInfo.name, arg, paramType);  
  17.         }  
  18.         else if ("".equals(arg) && (namedValueInfo.defaultValue != null)) {  
  19.             arg = resolveDefaultValue(namedValueInfo.defaultValue);  
  20.         }  
  21.   
  22.         if (binderFactory != null) {  
  23.             WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);  
  24.             arg = binder.convertIfNecessary(arg, paramType, parameter);  
  25.         }  
  26.   
  27.         handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);  
  28.   
  29.         return arg;  
  30.     }  

NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);獲取參數信息,就是按照@RequestParam的3個屬性來收集的,即defaultValue=null、required=false、name=date, 
Object arg = resolveName(namedValueInfo.name, parameter, webRequest);然後就是獲取原始數據,獲取過程如下:
 
Java代碼  收藏代碼
  1. @Override  
  2.     protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {  
  3.         Object arg;  
  4.   
  5.         HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);  
  6.         MultipartHttpServletRequest multipartRequest =  
  7.             WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);  
  8.   
  9.         if (MultipartFile.class.equals(parameter.getParameterType())) {  
  10.             assertIsMultipartRequest(servletRequest);  
  11.             Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");  
  12.             arg = multipartRequest.getFile(name);  
  13.         }  
  14.         else if (isMultipartFileCollection(parameter)) {  
  15.             assertIsMultipartRequest(servletRequest);  
  16.             Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");  
  17.             arg = multipartRequest.getFiles(name);  
  18.         }  
  19.         else if(isMultipartFileArray(parameter)) {  
  20.             assertIsMultipartRequest(servletRequest);  
  21.             Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");  
  22.             arg = multipartRequest.getFiles(name).toArray(new MultipartFile[0]);  
  23.         }  
  24.         else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) {  
  25.             assertIsMultipartRequest(servletRequest);  
  26.             arg = servletRequest.getPart(name);  
  27.         }  
  28.         else if (isPartCollection(parameter)) {  
  29.             assertIsMultipartRequest(servletRequest);  
  30.             arg = new ArrayList<Object>(servletRequest.getParts());  
  31.         }  
  32.         else if (isPartArray(parameter)) {  
  33.             assertIsMultipartRequest(servletRequest);  
  34.             arg = RequestPartResolver.resolvePart(servletRequest);  
  35.         }  
  36.         else {  
  37.             arg = null;  
  38.             if (multipartRequest != null) {  
  39.                 List<MultipartFile> files = multipartRequest.getFiles(name);  
  40.                 if (!files.isEmpty()) {  
  41.                     arg = (files.size() == 1 ? files.get(0) : files);  
  42.                 }  
  43.             }  
  44.             if (arg == null) {  
  45. //對於本工程,我們的重點在這裏這裏這裏這裏這裏這裏  
  46.                 String[] paramValues = webRequest.getParameterValues(name);  
  47.                 if (paramValues != null) {  
  48.                     arg = paramValues.length == 1 ? paramValues[0] : paramValues;  
  49.                 }  
  50.             }  
  51.         }  
  52.   
  53.         return arg;  
  54.     }  

通過webRequest.getParameterValues(name)來獲取原始的字符串。這裏便有涉及到了容器如tomcat的處理過程,這一獲取參數的過程在本系列的第五篇文章tomcat的獲取參數中進行了詳細的源碼介紹,那一篇主要是介紹亂碼的。本文章不再介紹,接着說,這樣就可以獲取到我們請求的原始字符串"2014---08---3 03:34:23",接下來便是執行轉換綁定的過程: 
Java代碼  收藏代碼
  1. if (binderFactory != null) {  
  2.             WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);  
  3.             arg = binder.convertIfNecessary(arg, paramType, parameter);  
  4.         }  

這一過程就是要尋找我們已經註冊的所有的PropertyEditor來進行轉換,如果還沒有找到,則使用另一套轉換流程,使用conversionService來進行轉換。我們慢慢來看這一過程,有了binderFactory便可以創建出WebDataBinder,具體的創建過程如下: 
Java代碼  收藏代碼
  1. public final WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName)  
  2.             throws Exception {  
  3.         WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);  
  4.         if (this.initializer != null) {  
  5.             this.initializer.initBinder(dataBinder, webRequest);  
  6.         }  
  7.         initBinder(dataBinder, webRequest);  
  8.         return dataBinder;  
  9.     }  

先創建出WebDataBinder,然後使用initializer的initBinder方法來初始化一些PropertyEditor,initializer的類型爲我們常見的ConfigurableWebBindingInitializer即在mvc:annotation-driven時默認註冊的最終設置爲RequestMappingHandlerAdapter的webBindingInitializer屬性值。this.initializer.initBinder(dataBinder, webRequest);過程如下: 
Java代碼  收藏代碼
  1. @Override  
  2.     public void initBinder(WebDataBinder binder, WebRequest request) {  
  3.         binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);  
  4.         if (this.directFieldAccess) {  
  5.             binder.initDirectFieldAccess();  
  6.         }  
  7.         if (this.messageCodesResolver != null) {  
  8.             binder.setMessageCodesResolver(this.messageCodesResolver);  
  9.         }  
  10.         if (this.bindingErrorProcessor != null) {  
  11.             binder.setBindingErrorProcessor(this.bindingErrorProcessor);  
  12.         }  
  13.         if (this.validator != null && binder.getTarget() != null &&  
  14.                 this.validator.supports(binder.getTarget().getClass())) {  
  15.             binder.setValidator(this.validator);  
  16.         }  
  17.         if (this.conversionService != null) {  
  18.             binder.setConversionService(this.conversionService);  
  19.         }  
  20.         if (this.propertyEditorRegistrars != null) {  
  21.             for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {  
  22.                 propertyEditorRegistrar.registerCustomEditors(binder);  
  23.             }  
  24.         }  
  25.     }  

即設置一些我們conversionService、messageCodesResolver、validator 等,這些參數即我們在mvc:annotation中進行設置的,若無設置,採用默認的。 
繼續執行initBinder(dataBinder, webRequest);
 
Java代碼  收藏代碼
  1. public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception {  
  2.         for (InvocableHandlerMethod binderMethod : this.binderMethods) {  
  3.             if (isBinderMethodApplicable(binderMethod, binder)) {  
  4.                 Object returnValue = binderMethod.invokeForRequest(request, null, binder);  
  5.                 if (returnValue != null) {  
  6.                     throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod);  
  7.                 }  
  8.             }  
  9.         }  
  10.     }  

執行那些適合我們已經創建的WebDataBinder,怎樣才叫適合的呢?看isBinderMethodApplicable(binderMethod, binder)方法 
Java代碼  收藏代碼
  1. protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder binder) {  
  2.         InitBinder annot = initBinderMethod.getMethodAnnotation(InitBinder.class);  
  3.         Collection<String> names = Arrays.asList(annot.value());  
  4.         return (names.size() == 0 || names.contains(binder.getObjectName()));  
  5.     }  

當initBinderMethod上的@InitBinder註解指定了value,該value可以是多個,當它包含了我們的方法的參數date,則這個initBinderMethod就會被執行。當@InitBinder註解沒有指定value,則也會被執行。所以爲了不用執行一些不必要的initBinderMethod,我們最好爲這些initBinderMethod上的@InitBinder加上value限定。對於我們寫的initBinder便因此開始執行了。 
由binderFactory創建出來的WebDataBinder就此完成,然後纔是詳細的轉換過程:
 
Java代碼  收藏代碼
  1. public <T> T convertIfNecessary(String propertyName, Object oldValue, Object newValue,  
  2.             Class<T> requiredType, TypeDescriptor typeDescriptor) throws IllegalArgumentException {  
  3.   
  4.         Object convertedValue = newValue;  
  5.   
  6.         // Custom editor for this type?  
  7.         PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);  
  8.   
  9.         ConversionFailedException firstAttemptEx = null;  
  10.   
  11.         // No custom editor but custom ConversionService specified?  
  12.         ConversionService conversionService = this.propertyEditorRegistry.getConversionService();  
  13.   
  14.             //略  
  15. }  

這裏首先使用已註冊的PropertyEditor,當仍然沒有找到時才使用ConversionService。對於本工程來說,由於已經手動註冊了對於Date的轉換的PropertyEditor即CustomDateEditor,然後便會執行CustomDateEditor的具體的轉換過程。至此,大體過程就算是完了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章