對于客戶端測試以前經常使用的方法是啟動一個內嵌的jetty/tomcat容器,然后發送真實的請求到相應的控制器;這種方式的缺點就是速度慢;自Spring 3.2開始提供了對RestTemplate的模擬服務器測試方式,也就是說使用RestTemplate測試時無須啟動服務器,而是模擬一個服務器進行測試,這樣的話速度是非??斓摹?
?
2 RestTemplate客戶端測試
整個環境在上一篇《 Spring MVC測試框架詳解——服務端測試 》基礎上進行構建。
?
- @RestController ??
- @RequestMapping ( "/users" )??
- public ? class ?UserRestController?{??
- ??
- ???? private ?UserService?userService;??
- ??
- ???? @Autowired ??
- ???? public ?UserRestController(UserService?userService)?{??
- ???????? this .userService?=?userService;??
- ????}??
- ??
- ???? @RequestMapping (value?=? "/{id}" ,?method?=?RequestMethod.GET,?produces?=?MediaType.APPLICATION_JSON_VALUE)??
- ???? public ?User?findById( @PathVariable ( "id" )?Long?id)?{??
- ???????? return ?userService.findById(1L);??
- ????}??
- ??
- ???? @RequestMapping (method?=?RequestMethod.POST)??
- ???? public ?ResponseEntity<User>?save( @RequestBody ?User?user,?UriComponentsBuilder?uriComponentsBuilder)?{??
- ???????? //save?user ??
- ????????user.setId(1L);??
- ????????MultiValueMap?headers?=? new ?HttpHeaders();??
- ????????headers.set( "Location" ,?uriComponentsBuilder.path( "/users/{id}" ).buildAndExpand(user.getId()).toUriString());??
- ???????? return ? new ?ResponseEntity(user,?headers,?HttpStatus.CREATED);??
- ????}??
- ??
- ???? @RequestMapping (value?=? "/{id}" ,?method?=?RequestMethod.PUT,?consumes?=?MediaType.APPLICATION_JSON_VALUE)??
- ???? @ResponseStatus (HttpStatus.NO_CONTENT)??
- ???? public ? void ?update( @RequestBody ?User?user)?{??
- ???????? //update?by?id ??
- ????}??
- ??
- ???? @RequestMapping (value?=? "/{id}" ,?method?=?RequestMethod.DELETE)??
- ???? public ? void ?delete( @PathVariable ( "id" )?Long?id)?{??
- ???????? //delete?by?id ??
- ????}??
- }??
?
- package ?com.sishuok.mvc.service;??
- ??
- import ?com.sishuok.mvc.entity.User;??
- ??
- public ? interface ?UserService?{??
- ???? public ?User?findById(Long?id);??
- }??
- package ?com.sishuok.mvc.service;??
- ??
- import ?com.sishuok.mvc.entity.User;??
- import ?org.springframework.stereotype.Service;??
- ??
- @Service ??
- public ? class ?UserServiceImpl? implements ?UserService?{??
- ??
- ???? public ?User?findById(Long?id)?{??
- ????????User?user?=? new ?User();??
- ????????user.setId(id);??
- ????????user.setName( "zhang" );??
- ???????? return ?user;??
- ????}??
- }??
- public ? abstract ? class ?AbstractClientTest?{??
- ??
- ???? static ?RestTemplate?restTemplate;??
- ????ObjectMapper?objectMapper;? //JSON ??
- ????Jaxb2Marshaller?marshaller;? //XML ??
- ????String?baseUri?=? "http://localhost:8080/users" ;??
- ??
- ???? @Before ??
- ???? public ? void ?setUp()? throws ?Exception?{??
- ????????objectMapper?=? new ?ObjectMapper();? //需要添加jackson?jar包 ??
- ??
- ????????marshaller?=? new ?Jaxb2Marshaller();? //需要添加jaxb2實現(如xstream) ??
- ????????marshaller.setPackagesToScan( new ?String[]?{ "com.sishuok" });??
- ????????marshaller.afterPropertiesSet();??
- ??
- ????????restTemplate?=? new ?RestTemplate();??
- ????}??
- }??
?
2.1 使用內嵌Jetty方式啟動容器進行
需要添加jetty依賴:
- <dependency>??
- ????<groupId>org.eclipse.jetty</groupId>??
- ????<artifactId>jetty-server</artifactId>??
- ????<version>${jetty.version}</version>??
- ????<scope>test</scope>??
- </dependency>??
- <dependency>??
- ????<groupId>org.eclipse.jetty</groupId>??
- ????<artifactId>jetty-webapp</artifactId>??
- ????<version>${jetty.version}</version>??
- ????<scope>test</scope>??
- </dependency>??
?
如果要測試JSP,請添加
- <dependency>??
- ????<groupId>org.eclipse.jetty</groupId>??
- ????<artifactId>jetty-jsp</artifactId>??
- ????<version>${jetty.version}</version>??
- ????<scope>test</scope>??
- </dependency>??
版本:<jetty.version>8.1.8.v20121106</jetty.version>
?
?
測試示例( EmbeddedJettyClientTest.java )
- public ? class ?EmbeddedJettyClientTest? extends ?AbstractClientTest?{??
- ??
- ???? private ? static ?Server?server;??
- ??
- ???? @BeforeClass ??
- ???? public ? static ? void ?beforeClass()? throws ?Exception?{??
- ???????? //創建一個server ??
- ????????server?=? new ?Server( 8080 );??
- ????????WebAppContext?context?=? new ?WebAppContext();??
- ????????String?webapp?=? "spring-mvc-test/src/main/webapp" ;??
- ????????context.setDescriptor(webapp?+? "/WEB-INF/web.xml" );?? //指定web.xml配置文件 ??
- ????????context.setResourceBase(webapp);?? //指定webapp目錄 ??
- ????????context.setContextPath( "/" );??
- ????????context.setParentLoaderPriority( true );??
- ??
- ????????server.setHandler(context);??
- ????????server.start();??
- ????}??
- ??
- ???? @AfterClass ??
- ???? public ? static ? void ?afterClass()? throws ?Exception?{??
- ????????server.stop();? //當測試結束時停止服務器 ??
- ????}??
- ??
- ???? @Test ??
- ???? public ? void ?testFindById()? throws ?Exception?{??
- ????????ResponseEntity<User>?entity?=?restTemplate.getForEntity(baseUri?+? "/{id}" ,?User. class ,?1L);??
- ??
- ????????assertEquals(HttpStatus.OK,?entity.getStatusCode());??
- ????????assertThat(entity.getHeaders().getContentType().toString(),?containsString(MediaType.APPLICATION_JSON_VALUE));??
- ????????assertThat(entity.getBody(),?hasProperty( "name" ,?is( "zhang" )));??
- ????}??
- ???? //省略其他,請參考github ??
- }??
?
此處通過內嵌Jetty啟動一個web容器,然后使用RestTemplate訪問真實的uri進行訪問,然后進行斷言驗證。
?
這種方式的最大的缺點是如果我只測試UserRestController,其他的組件也會加載,屬于集成測試,速度非常慢。伴隨著Spring Boot項目的發布,我們可以使用Spring Boot進行測試。
?
2.2 使用Spring Boot進行測試
spring boot請參考 spring boot官網 ?和《 Spring Boot——2分鐘構建spring web mvc REST風格HelloWorld 》進行入門。通過spring boot我們可以只加載某個控制器進行測試。更加方便。
?
添加spring-boot-starter-web依賴:
- <dependency>??
- ????<groupId>org.springframework.boot</groupId>??
- ????<artifactId>spring-boot-starter-web</artifactId>??
- ????<version>${spring.boot.version}</version>??
- ????<scope>test</scope>??
- </dependency>??
版本:<spring.boot.version>0.5.0.BUILD-SNAPSHOT</spring.boot.version>,目前還處于SNAPSHOT版本。
?
?
測試示例( SpringBootClientTest.java )
- public ? class ?SpringBootClientTest? extends ?AbstractClientTest?{??
- ??
- ???? private ? static ?ApplicationContext?ctx;??
- ??
- ???? @BeforeClass ??
- ???? public ? static ? void ?beforeClass()? throws ?Exception?{??
- ????????ctx?=?SpringApplication.run(Config. class );? //啟動服務器?加載Config指定的組件 ??
- ????}??
- ??
- ???? @AfterClass ??
- ???? public ? static ? void ?afterClass()? throws ?Exception?{??
- ????????SpringApplication.exit(ctx); //退出服務器 ??
- ????}??
- ??
- ??
- ???? @Test ??
- ???? public ? void ?testFindById()? throws ?Exception?{??
- ????????ResponseEntity<User>?entity?=?restTemplate.getForEntity(baseUri?+? "/{id}" ,?User. class ,?1L);??
- ??
- ????????assertEquals(HttpStatus.OK,?entity.getStatusCode());??
- ????????assertThat(entity.getHeaders().getContentType().toString(),?containsString(MediaType.APPLICATION_JSON_VALUE));??
- ????????assertThat(entity.getBody(),?hasProperty( "name" ,?is( "zhang" )));??
- ????}??
- ??
- ???? //省略其他,請參考github ??
- ?????
- ???? @Configuration ??
- ???? @EnableAutoConfiguration ??
- ???? static ? class ?Config?{??
- ??
- ???????? @Bean ??
- ???????? public ?EmbeddedServletContainerFactory?servletContainer()?{??
- ???????????? return ? new ?JettyEmbeddedServletContainerFactory();??
- ????????}??
- ??
- ???????? @Bean ??
- ???????? public ?UserRestController?userController()?{??
- ???????????? return ? new ?UserRestController(userService());??
- ????????}??
- ??
- ???????? @Bean ??
- ???????? public ?UserService?userService()?{??
- ???????????? //Mockito請參考?http://stamen.iteye.com/blog/1470066 ??
- ????????????UserService?userService?=?Mockito.mock(UserService. class );??
- ????????????User?user?=? new ?User();??
- ????????????user.setId(1L);??
- ????????????user.setName( "zhang" );??
- ????????????Mockito.when(userService.findById(Mockito.any(Long. class ))).thenReturn(user);??
- ???????????? return ?userService;??
- //????????????return?new?UserServiceImpl();?//此處也可以返回真實的UserService實現 ??
- ????????}??
- ????}??
- ??
- }??
?
通過SpringApplication.run啟動一個服務器,然后Config.xml是Spring的Java配置方式,此處只加載了UserRestController及其依賴UserService,對于UserService可以通過如Mockito進行模擬/也可以注入真實的實現,Mockito請參考《 單元測試系列之2:模擬利器Mockito 》。可以通過EmbeddedServletContainerFactory子類指定使用哪個內嵌的web容器(目前支持:jetty/tomcat)。
?
這種方式的優點就是速度比內嵌Jetty容器速度快,但是還是不夠快且還需要啟動一個服務器(開一個端口),因此Spring 3.2提供了模擬Server的方式進行測試。即服務器是通過Mock技術模擬的而不是真的啟動一個服務器。
?
上述兩種方式對于如果服務還不存在的情況也是無法測試的,因此Mock Server進行測試時最好的選擇。
?
2.3 使用Mock Service Server進行測試
通過Mock Service Server方式的優點:
不需要啟動服務器;
可以在服務還沒寫好的情況下進行測試,這樣可以進行并行開發/測試。
?
對于Mock Service Server主要操作步驟:
1、通過MockRestServiceServer創建RestTemplate的Mock Server;
2、添加客戶端請求斷言,即用于判斷客戶端請求的斷言;
3、添加服務端響應,即返回給客戶端的響應;
?
為了方便測試,請靜態導入:
- import ? static ?org.springframework.test.web.client.*;??
- import ? static ?org.springframework.test.web.client.match.MockRestRequestMatchers.*;??
- import ? static ?org.springframework.test.web.client.response.MockRestResponseCreators.*;??
?
測試示例( MockServerClientTest.java )
- public ? class ?MockServerClientTest? extends ?AbstractClientTest?{??
- ??
- ???? private ?MockRestServiceServer?mockServer;??
- ??
- ???? @Before ??
- ???? public ? void ?setUp()? throws ?Exception?{??
- ???????? super .setUp();??
- ???????? //模擬一個服務器 ??
- ????????mockServer?=?createServer(restTemplate);??
- ????}??
- ??
- ???? @Test ??
- ???? public ? void ?testFindById()? throws ?JsonProcessingException?{??
- ????????String?uri?=?baseUri?+? "/{id}" ;??
- ????????Long?id?=?1L;??
- ????????User?user?=? new ?User();??
- ????????user.setId(1L);??
- ????????user.setName( "zhang" );??
- ????????String?userJson?=?objectMapper.writeValueAsString(user);??
- ????????String?requestUri?=?UriComponentsBuilder.fromUriString(uri).buildAndExpand(id).toUriString();??
- ??
- ???????? //添加服務器端斷言 ??
- ????????mockServer??
- ????????????????.expect(requestTo(requestUri))??
- ????????????????.andExpect(method(HttpMethod.GET))??
- ????????????????.andRespond(withSuccess(userJson,?MediaType.APPLICATION_JSON));??
- ??
- ???????? //2、訪問URI(與API交互) ??
- ????????ResponseEntity<User>?entity?=?restTemplate.getForEntity(uri,?User. class ,?id);??
- ??
- ???????? //3.1、客戶端驗證 ??
- ????????assertEquals(HttpStatus.OK,?entity.getStatusCode());??
- ????????assertThat(entity.getHeaders().getContentType().toString(),?containsString(MediaType.APPLICATION_JSON_VALUE));??
- ????????assertThat(entity.getBody(),?hasProperty( "name" ,?is( "zhang" )));??
- ??
- ???????? //3.2、服務器端驗證(驗證之前添加的服務器端斷言) ??
- ????????mockServer.verify();??
- ????}??
- ???? //省略其他,請參考github ??
- }??
??
測試步驟:
1、準備測試環境
首先創建RestTemplate,然后通過MockRestServiceServer.createServer(restTemplate)創建一個Mock Server,其會自動設置restTemplate的requestFactory為RequestMatcherClientHttpRequestFactory(restTemplate發送請求時都通過ClientHttpRequestFactory創建ClientHttpRequest)。
2、調用API
即restTemplate.getForEntity(uri, User.class, id)訪問rest web service;
3、斷言驗證
3.1、客戶端請求斷言驗證
如mockServer.expect(requestTo(requestUri)).andExpect(method(HttpMethod.GET)):即會驗證之后通過restTemplate發送請求的uri是requestUri,且請求方法是GET;
3.2、服務端響應斷言驗證
首先通過mockServer.andRespond(withSuccess(new ObjectMapper().writeValueAsString(user), MediaType.APPLICATION_JSON));返回給客戶端響應信息;
然后restTemplate就可以得到ResponseEntity,之后就可以通過斷言進行驗證了;
4、 卸載測試環境
?
對于單元測試步驟請參考: 加速Java應用開發速度3——單元/集成測試+CI 。
?
2.4 了解測試API
?
MockRestServiceServer
用來創建模擬服務器,其提供了createServer(RestTemplate restTemplate),傳入一個restTemplate即可創建一個MockRestServiceServer;在createServer中:
- MockRestServiceServer?mockServer?=? new ?MockRestServiceServer();??
- RequestMatcherClientHttpRequestFactory?factory?=?mockServer. new ?RequestMatcherClientHttpRequestFactory();??
- ??
- restTemplate.setRequestFactory(factory);??
?
?
RequestMatcher/MockRestRequestMatchers
RequestMatcher用于驗證請求信息的驗證器,即RestTemplate發送的請求的URI、請求方法、請求的Body體內容等等;spring mvc測試框架提供了很多***RequestMatchers來滿足測試需求;類似于《Spring MVC測試框架詳解——服務端測試》中的***ResultMatchers;注意這些***RequestMatchers并不是ResultMatcher的子類,而是返回RequestMatcher實例的。Spring mvc測試框架為了測試方便提供了MockRestRequestMatchers靜態工廠方法方便操作;具體的API如下:
RequestMatcher anything():即請求可以是任何東西;
RequestMatcher requestTo(final Matcher<String> matcher)/RequestMatcher requestTo(final String expectedUri)/RequestMatcher requestTo(final URI uri):請求URI必須匹配某個Matcher/uri字符串/URI;
RequestMatcher method(final HttpMethod method):請求方法必須匹配某個請求方法;
RequestMatcher header(final String name, final Matcher<? super String>... matchers)/RequestMatcher header(final String name, final String... expectedValues):請求頭必須匹配某個Matcher/某些值;
ContentRequestMatchers content():獲取內容匹配器,然后可以通過如contentType(String expectedContentType)進行ContentType匹配等,具體請參考javadoc;
JsonPathRequestMatchers jsonPath(String expression, Object ... args)/RequestMatcher jsonPath(String expression, Matcher<T> matcher):獲取Json路徑匹配器/直接進行路徑匹配,具體請參考javadoc;
XpathRequestMatchers xpath(String expression, Object... args)/XpathRequestMatchers xpath(String expression, Map<String, String> namespaces, Object... args):獲取Xpath表達式匹配器/直接進行Xpath表達式匹配,具體請參考javadoc;
?
ResponseCreator/MockRestResponseCreators
ResponseCreator用于創建返回給客戶端的響應信息,spring mvc提供了靜態工廠方法MockRestResponseCreators進行操作;具體的API如下:
DefaultResponseCreator withSuccess() :返回給客戶端200(OK)狀態碼響應;
DefaultResponseCreator withSuccess(String body, MediaType mediaType)/DefaultResponseCreator withSuccess(byte[] body, MediaType contentType)/DefaultResponseCreator withSuccess(Resource body, MediaType contentType):返回給客戶端200(OK)狀態碼響應,且返回響應內容體和MediaType;
DefaultResponseCreator withCreatedEntity(URI location):返回201(Created)狀態碼響應,并返回響應頭“Location=location";
DefaultResponseCreator withNoContent() :返回204(NO_CONTENT)狀態碼響應;
DefaultResponseCreator withBadRequest() :返回400(BAD_REQUEST)狀態碼響應;
DefaultResponseCreator withUnauthorizedRequest()?:返回401(UNAUTHORIZED)狀態碼響應;
DefaultResponseCreator withServerError() :返回500(SERVER_ERROR)狀態碼響應;
DefaultResponseCreator withStatus(HttpStatus status):設置自定義狀態碼;
?
對于DefaultResponseCreator還提供了如下API:
DefaultResponseCreator body(String content) /DefaultResponseCreator body(byte[] content)/DefaultResponseCreator body(Resource resource):內容體響應,對于String content 默認是UTF-8編碼的;
DefaultResponseCreator contentType(MediaType mediaType) :響應的ContentType;
DefaultResponseCreator location(URI location)?:響應的Location頭;
DefaultResponseCreator headers(HttpHeaders headers):設置響應頭;
?
2.5 測試示例
?
測試查找
請參考之前的testFindById;
?
測試新增
提交JSON數據進行新增
- @Test ??
- public ? void ?testSaveWithJson()? throws ?Exception?{??
- ????User?user?=? new ?User();??
- ????user.setId(1L);??
- ????user.setName( "zhang" );??
- ????String?userJson?=?objectMapper.writeValueAsString(user);??
- ??
- ????String?uri?=?baseUri;??
- ????String?createdLocation?=?baseUri?+? "/" ?+? 1 ;??
- ??
- ????mockServer??
- ????????????.expect(requestTo(uri))?? //驗證請求URI ??
- ????????????.andExpect(jsonPath( "$.name" ).value(user.getName()))? //驗證請求的JSON數據 ??
- ????????????.andRespond(withCreatedEntity(URI.create(createdLocation)).body(userJson).contentType(MediaType.APPLICATION_JSON));? //添加響應信息 ??
- ??
- ??
- ????restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList( new ?MappingJackson2HttpMessageConverter()));??
- ????ResponseEntity<User>?responseEntity?=?restTemplate.postForEntity(uri,?user,?User. class );??
- ??
- ????assertEquals(createdLocation,?responseEntity.getHeaders().get( "Location" ).get( 0 ));??
- ????assertEquals(HttpStatus.CREATED,?responseEntity.getStatusCode());??
- ????assertEquals(user,?responseEntity.getBody());??
- ??
- ????mockServer.verify();??
- }??
提交XML數據進行新增
- @Test ??
- public ? void ?testSaveWithXML()? throws ?Exception?{??
- ????User?user?=? new ?User();??
- ????user.setId(1L);??
- ????user.setName( "zhang" );??
- ????ByteArrayOutputStream?bos?=? new ?ByteArrayOutputStream();??
- ????marshaller.marshal(user,? new ?StreamResult(bos));??
- ????String?userXml?=?bos.toString();??
- ??
- ????String?uri?=?baseUri;??
- ????String?createdLocation?=?baseUri?+? "/" ?+? 1 ;??
- ??
- ????mockServer??
- ????????????.expect(requestTo(uri))?? //驗證請求URI ??
- ????????????.andExpect(xpath( "/user/name/text()" ).string(user.getName()))? //驗證請求的JSON數據 ??
- ????????????.andRespond(withCreatedEntity(URI.create(createdLocation)).body(userXml).contentType(MediaType.APPLICATION_XML));? //添加響應信息 ??
- ??
- ????restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList( new ?Jaxb2RootElementHttpMessageConverter()));??
- ????ResponseEntity<User>?responseEntity?=?restTemplate.postForEntity(uri,?user,?User. class );??
- ??
- ????assertEquals(createdLocation,?responseEntity.getHeaders().get( "Location" ).get( 0 ));??
- ????assertEquals(HttpStatus.CREATED,?responseEntity.getStatusCode());??
- ??
- ????assertEquals(user,?responseEntity.getBody());??
- ??
- ????mockServer.verify();??
- }??
??
測試修改?
- @Test ??
- public ? void ?testUpdate()? throws ?Exception?{??
- ????User?user?=? new ?User();??
- ????user.setId(1L);??
- ????user.setName( "zhang" );??
- ??
- ????String?uri?=?baseUri?+? "/{id}" ;??
- ??
- ????mockServer??
- ????????????.expect(requestTo(uri))?? //驗證請求URI ??
- ????????????.andExpect(jsonPath( "$.name" ).value(user.getName()))? //驗證請求的JSON數據 ??
- ????????????.andRespond(withNoContent());? //添加響應信息 ??
- ??
- ????restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList( new ?MappingJackson2HttpMessageConverter()));??
- ????ResponseEntity?responseEntity?=?restTemplate.exchange(uri,?HttpMethod.PUT,? new ?HttpEntity<>(user),?(Class)? null ,?user.getId());??
- ??
- ????assertEquals(HttpStatus.NO_CONTENT,?responseEntity.getStatusCode());??
- ??
- ????mockServer.verify();??
- }??
?
測試刪除?
- @Test ??
- public ? void ?testDelete()? throws ?Exception?{??
- ????String?uri?=?baseUri?+? "/{id}" ;??
- ????Long?id?=?1L;??
- ??
- ????mockServer??
- ????????????.expect(requestTo(baseUri?+? "/" ?+?id))?? //驗證請求URI ??
- ????????????.andRespond(withSuccess());? //添加響應信息 ??
- ??
- ????ResponseEntity?responseEntity?=?restTemplate.exchange(uri,?HttpMethod.DELETE,?HttpEntity.EMPTY,?(Class)? null ,?id);??
- ????assertEquals(HttpStatus.OK,?responseEntity.getStatusCode());??
- ??
- ????mockServer.verify();??
- }??
?
通過Mock Server的最大好處是不需要啟動服務器,且不需要服務預先存在就可以測試;如果服務已經存在,通過Spring Boot進行測試也是個不錯的選擇。
?
?
再來回顧下測試步驟
1、準備測試環境
首先創建RestTemplate,然后通過MockRestServiceServer.createServer(restTemplate)創建一個Mock Server,其會自動設置restTemplate的requestFactory為RequestMatcherClientHttpRequestFactory(restTemplate發送請求時都通過ClientHttpRequestFactory創建ClientHttpRequest)。
2、調用API
即restTemplate.getForEntity(uri, User.class, id)訪問rest web service;
3、斷言驗證
3.1、客戶端請求斷言驗證
如mockServer.expect(requestTo(requestUri)).andExpect(method(HttpMethod.GET)):即會驗證之后通過restTemplate發送請求的uri是requestUri,且請求方法是GET;
3.2、服務端響應斷言驗證
首先通過mockServer.andRespond(withSuccess(new ObjectMapper().writeValueAsString(user), MediaType.APPLICATION_JSON));返回給客戶端響應信息;
然后restTemplate就可以得到ResponseEntity,之后就可以通過斷言進行驗證了;
4、 卸載測試環境
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
