SpringBootJava

SpringBoot实现RESTful与RestTemplate

2019-07-01  本文已影响2人  maxzhao_
title: SpringBoot+JPA多数据源(注解方式)
date: 2019-06-27
author: maxzhao
tags:
  - JAVA
  - SpringBoot
  - RESTful
  - RestTemplate
  - 编写接口测试类
categories:
  - SpringBoot

RESTful

介绍

REST:表现层状态转化(Representational State Transfer)

RESTful是一种软件设计风格,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便。

Spring 对 RESTful 风格的接口有着天然的支持。

什么是REST

REST(RepresentationalState Transfer)是Roy Fielding 提出的一个描述互联系统架构风格的名词。REST定义了一组体系架构原则,您可以根据这些原则设计以系统资源为中心的Web 服务,包括使用不同语言编写的客户端如何通过 HTTP处理和传输资源状态。

为什么称为 REST?Web本质上由各种各样的资源组成,资源由URI 唯一标识。浏览器(或者任何其它类似于浏览器的应用程序)将展示出该资源的一种表现方式,或者一种表现状态。如果用户在该页面中定向到指向其它资源的链接,则将访问该资源,并表现出它的状态。这意味着客户端应用程序随着每个资源表现状态的不同而发生状态转移,也即所谓REST。

附:REST定义RESTSOAP的比较

REST成熟度的四个层次

第一个层次(Level0)的Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形 式。SOAP和 XML-RPC都属于此类。

第二个层次(Level1)的Web 服务引入了资源的概念。每个资源有对应的标识符和表达。

第三个层次(Level2)的Web 服务使用不同的 HTTP 方法来进行不同的操作,并且使用HTTP 状态码来表示不同的结果。如 HTTPGET 方法来获取资源,HTTPDELETE 方法来删除资源。

第四个层次(Level3)的Web 服务使用 HATEOAS。在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作。

其中第三个层次建立了创建、读取、更新和删除(create,read, update, and delete,CRUD)操作与 HTTP方法之间的一对一映射。根据此映射:

(1)若要在服务器上创建资源,应该使用POST 方法。

(2)若要检索某个资源,应该使用GET 方法。

(3)若要更改资源状态或对其进行更新,应该使用PUT 方法。

(4)若要删除某个资源,应该使用DELETE 方法。

HTTP请求的方法

(1)GET:通过请求URI得到资源
(2)POST:用于添加新的内容
(3)PUT:用于修改某个内容,若不存在则添加
(4)DELETE:删除某个内容
(5)OPTIONS :询问可以执行哪些方法
(6)HEAD :类似于GET, 但是不返回body信息,用于检查对象是否存在,以及得到对象的元数据
(7)CONNECT :用于代理进行传输,如使用SSL
(8)TRACE:用于远程诊断服务器

HTTP请求的状态码

(1)成功Successful2xx:此类状态码标识客户端的请求被成功接收、理解并接受。常见如200(OK)、204(NoContent)。
(2)重定向Redirection3xx:这个类别的状态码标识用户代理要做出进一步的动作来完成请求。常见如301(MovedPermanently)、302(MovedTemprarily)。
(3)客户端错误Client Error 4xx:4xx类别的状态码是当客户端象是出错的时使用的。常见如400(BadRequest)、401(Unauthorized)、403(Forbidden)、404(NotFound)。
(4)服务器错误Server Error 5xx:响应状态码以5开头表示服务器知道自己出错或者没有能力执行请求。常见如500(InternalServer Error)、502(BadGateway)、504(GatewayTimeout)。

附:HTTP1.1的标准简介

接口中的注解

  1. @Controller:修饰class,用来创建处理http请求的对象
  2. @RestController:Spring4之后加入的注解,原来在@Controller中返回实体需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回实体格式。
  3. @RequestMapping:配置 url 映射
  4. @PostMapping: 这个是@RequestMapping+POST方法的简写
  5. @RequestHeader: 请求Header参数
  6. @PathVariable: URL路径参数,比如/{id}中的id参数
  7. @RequestParam: URL请求参数
  8. @RequestBody: 请求Body参数

RESTful架构有一些典型的设计误区,就是URI包含动词。因为”资源”表示一种实体,所以应该是名词,URI不应该有动词,动词应该放在HTTP协议中。 上面设计的API的URI中都是名词。(此句引用)

RESTful 测试类


@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApplicationTests {
    @Autowired
    private MockMvc mvc;

    @Test
    public void testUserController() throws Exception {
        // 测试UserController
        RequestBuilder request;

        // 1、get查一下user列表,应该为空
        request = get("/users/");
        MvcResult result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        String content = result.getResponse().getContentAsString();
        BaseResponse<List<User>> response = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<List<User>>>() {});
        assertThat(response.isSuccess(), is(true));
        assertThat(response.getMsg(), is("查询列表成功"));
        assertThat(((List) response.getData()).size(), is(0));

        // 2、post提交一个user
        request = post("/users/")
                .param("id", "1")
                .param("name", "测试大师")
                .param("age", "20");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<String> response1 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<String>>() {});
        assertThat(response1.isSuccess(), is(true));
        assertThat(response1.getMsg(), is("新增成功"));

        // 3、get获取user列表,应该有刚才插入的数据
        request = get("/users/");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<List<User>> response2 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<List<User>>>() {});
        assertThat(response2.isSuccess(), is(true));
        assertThat(response2.getMsg(), is("查询列表成功"));
        assertThat((response2.getData()).size(), is(1));

        // 4、put修改id为1的user
        request = put("/users/1")
                .param("name", "测试终极大师")
                .param("age", "30");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<String> response3 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<String>>() {});
        assertThat(response3.isSuccess(), is(true));
        assertThat(response3.getMsg(), is("更新成功"));

        // 5、get一个id为1的user
        request = get("/users/1");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<User> response4 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<User>>() {});
        assertThat(response4.isSuccess(), is(true));
        assertThat(response4.getMsg(), is("查询成功"));
        User user = response4.getData();
        assertThat(user.getId(), is(1L));
        assertThat(user.getName(), is("测试终极大师"));

        // 6、del删除id为1的user
        request = delete("/users/1");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<String> response5 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<String>>() {});
        assertThat(response5.isSuccess(), is(true));
        assertThat(response5.getMsg(), is("删除成功"));

        // 7、get查一下user列表,应该为空
        request = get("/users/");
        result = mvc.perform(request)
                .andExpect(status().isOk())
                .andReturn();
        content = result.getResponse().getContentAsString();
        BaseResponse<List<User>> response6 = JacksonUtil.json2Bean(content, new TypeReference<BaseResponse<List<User>>>() {});
        assertThat(response6.isSuccess(), is(true));
        assertThat(response6.getMsg(), is("查询列表成功"));
        assertThat((response6.getData()).size(), is(0));
    }

}

RestTemplate

介绍

用于同步客户端HTTP访问的Spring scentral类。它简化了与httpserver的通信,并实施了RESTful原则。它处理HTTP连接,让应用程序代码提供url(带有可能的模板变量)并提取结果。(简化了发起HTTP请求以及处理响应的过程,并且支持REST。)

RestTemplate默认依赖JDK提供http连接的能力(HttpURLConnection),如果有需要的话也可以通过setRequestFactory方法替换为例如 Apache HttpComponents、Netty或OkHttp等其它HTTP library。

实现逻辑

参考https://www.xncoding.com/2017/07/06/spring/sb-restclient.html

不同的 HTTP 请求实现方式

需要引入依赖

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <!--<version>4.5.3</version>-->
</dependency>

URLConnection

String result= "";
        BufferedReaderin = null;
        try {
            String urlNameString= url +"?" + param;
            URL realUrl= new URL(urlNameString);
            // 打开和URL之间的连接
            URLConnectionconnection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept","*/*");
            connection.setRequestProperty("connection","Keep-Alive");
            connection.setRequestProperty("user-agent",
                    "Mozilla/4.0(compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 建立实际的连接
            connection.connect();
            // 获取所有响应头字段
            Map<String,List<String>> map = connection.getHeaderFields();
            // 遍历所有的响应头字段
            for(String key : map.keySet()) {
                System.out.println(key+ "--->" + map.get(key));
            }
            // 定义 BufferedReader输入流来读取URL的响应
            in =new BufferedReader(newInputStreamReader(
                    connection.getInputStream()));
            String line;
            while ((line = in.readLine())!= null) {
                result += line;
            }
        } catch (Exception e) {
            …
        }
        // 使用finally块来关闭输入流
        finally{
         // 关闭流
        }

RestTemplate

ResponseEntity<SsoUrlPrm>result = restTemplate.getForEntity(requestPathUrl,SsoUrlPrm.class);  

参考1https://www.cnblogs.com/zhaoyan001/p/8442602.html

参考2https://blog.csdn.net/weixin_39986856/article/details/83018655

使用

直接使用方式很简单:

public class RestTemplateTest { public static void main(String[] args) {
    RestTemplate restT = new RestTemplate();
    //通过Jackson JSON processing library直接将返回值绑定到对象  
    Quote quote = restT.getForObject("http://gturnquist-quoters.cfapps.io/api/random", Quote.class);
    String quoteString = restT.getForObject("http://gturnquist-quoters.cfapps.io/api/random", String.class);
    System.out.println(quoteString);
}}

发送GET请求

// 1-getForObject()
User user1 = this.restTemplate.getForObject(uri, User.class);
// 2-getForEntity()
ResponseEntity<User> responseEntity1 = this.restTemplate.getForEntity(uri, User.class);HttpStatus statusCode = responseEntity1.getStatusCode();HttpHeaders header = responseEntity1.getHeaders();User user2 = responseEntity1.getBody();  
// 3-exchange()
RequestEntity requestEntity = RequestEntity.get(new URI(uri)).build();ResponseEntity<User> responseEntity2 = this.restTemplate.exchange(requestEntity, User.class);User user3 = responseEntity2.getBody();

发送POST请求

// 1-postForObject()
User user1 = this.restTemplate.postForObject(uri, user, User.class);
// 2-postForEntity()
ResponseEntity<User> responseEntity1 = this.restTemplate.postForEntity(uri, user, User.class);
// 3-exchange()
RequestEntity<User> requestEntity = RequestEntity
.post(new URI(uri))
    .body(user);
ResponseEntity<User> responseEntity2 = this.restTemplate.exchange(requestEntity, User.class);

设置HTTP Header

// 1-Content-TypeRequest
Entity<User> requestEntity = RequestEntity        
.post(new URI(uri))        
    .contentType(MediaType.APPLICATION_JSON)        
    .body(user);
// 2-AcceptRequest
Entity<User> requestEntity = RequestEntity
.post(new URI(uri))
    .accept(MediaType.APPLICATION_JSON)
    .body(user);
// 3-OtherRequest
Entity<User> requestEntity = RequestEntity
.post(new URI(uri))        
    .header("Authorization", "Basic " + base64Credentials)        
    .body(user);

捕获异常

捕获HttpServerErrorException

try {    
    responseEntity = restTemplate.exchange(requestEntity, String.class);} catch (HttpServerErrorException e) {
    // log error
}

自定义异常处理器

public class CustomErrorHandler extends DefaultResponseErrorHandler {    @Override    
    public void handleError(ClientHttpResponse response) throws IOException {
    // todo    
}}

然后设置下异常处理器:

@Configurationpublic class RestClientConfig {
    @Bean    
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new CustomErrorHandler());
        return restTemplate;
    }
}

配置类

创建HttpClientConfig类,设置连接池大小、超时时间、重试机制等。配置如下:

/**
 * - Supports both HTTP and HTTPS
 * - Uses a connection pool to re-use connections and save overhead of creating connections.
 * - Has a custom connection keep-alive strategy (to apply a default keep-alive if one isn't specified)
 * - Starts an idle connection monitor to continuously clean up stale connections.
 */
@Configuration
@EnableScheduling
public class HttpClientConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientConfig.class);

    @Resource
    private HttpClientProperties p;

    @Bean
    public PoolingHttpClientConnectionManager poolingConnectionManager() {
        SSLContextBuilder builder = new SSLContextBuilder();
        try {
            builder.loadTrustMaterial(null, new TrustStrategy() {
                public boolean isTrusted(X509Certificate[] arg0, String arg1) {
                    return true;
                }
            });
        } catch (NoSuchAlgorithmException | KeyStoreException e) {
            LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e);
        }

        SSLConnectionSocketFactory sslsf = null;
        try {
            sslsf = new SSLConnectionSocketFactory(builder.build());
        } catch (KeyManagementException | NoSuchAlgorithmException e) {
            LOGGER.error("Pooling Connection Manager Initialisation failure because of " + e.getMessage(), e);
        }

        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
                .<ConnectionSocketFactory>create()
                .register("https", sslsf)
                .register("http", new PlainConnectionSocketFactory())
                .build();

        PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        poolingConnectionManager.setMaxTotal(p.getMaxTotalConnections());  //最大连接数
        poolingConnectionManager.setDefaultMaxPerRoute(p.getDefaultMaxPerRoute());  //同路由并发数
        return poolingConnectionManager;
    }

    @Bean
    public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
        return new ConnectionKeepAliveStrategy() {
            @Override
            public long getKeepAliveDuration(HttpResponse response, HttpContext httpContext) {
                HeaderElementIterator it = new BasicHeaderElementIterator
                        (response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                while (it.hasNext()) {
                    HeaderElement he = it.nextElement();
                    String param = he.getName();
                    String value = he.getValue();
                    if (value != null && param.equalsIgnoreCase("timeout")) {
                        return Long.parseLong(value) * 1000;
                    }
                }
                return p.getDefaultKeepAliveTimeMillis();
            }
        };
    }

    @Bean
    public CloseableHttpClient httpClient() {
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(p.getRequestTimeout())
                .setConnectTimeout(p.getConnectTimeout())
                .setSocketTimeout(p.getSocketTimeout()).build();

        return HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(poolingConnectionManager())
                .setKeepAliveStrategy(connectionKeepAliveStrategy())
                .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))  // 重试次数
                .build();
    }

    @Bean
    public Runnable idleConnectionMonitor(final PoolingHttpClientConnectionManager connectionManager) {
        return new Runnable() {
            @Override
            @Scheduled(fixedDelay = 10000)
            public void run() {
                try {
                    if (connectionManager != null) {
                        LOGGER.trace("run IdleConnectionMonitor - Closing expired and idle connections...");
                        connectionManager.closeExpiredConnections();
                        connectionManager.closeIdleConnections(p.getCloseIdleConnectionWaitTimeSecs(), TimeUnit.SECONDS);
                    } else {
                        LOGGER.trace("run IdleConnectionMonitor - Http Client Connection manager is not initialised");
                    }
                } catch (Exception e) {
                    LOGGER.error("run IdleConnectionMonitor - Exception occurred. msg={}, e={}", e.getMessage(), e);
                }
            }
        };
    }

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("poolScheduler");
        scheduler.setPoolSize(50);
        return scheduler;
    }
}

然后再配置RestTemplateConfig类,使用之前配置好的CloseableHttpClient类注入,同时配置一些默认的消息转换器:

/**
 * RestTemplate客户端连接池配置
 */
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class RestTemplateConfig {

    @Resource
    private CloseableHttpClient httpClient;

    @Bean
    public RestTemplate restTemplate(MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) {
        RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory());

        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("utf-8"));
        messageConverters.add(stringHttpMessageConverter);
        messageConverters.add(jackson2HttpMessageConverter);
        restTemplate.setMessageConverters(messageConverters);

        return restTemplate;
    }

    @Bean
    public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
        HttpComponentsClientHttpRequestFactory rf = new HttpComponentsClientHttpRequestFactory();
        rf.setHttpClient(httpClient);
        return rf;
    }

}

发送文件

MultiValueMap<String, Object> multiPartBody = new LinkedMultiValueMap<>();
multiPartBody.add("file", new ClassPathResource("/tmp/user.txt"));
RequestEntity<MultiValueMap<String, Object>> requestEntity = RequestEntity        
.post(uri)        
    .contentType(MediaType.MULTIPART_FORM_DATA)        
    .body(multiPartBody);

下载文件

// 小文件
RequestEntity requestEntity = RequestEntity.get(uri).build();ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);byte[] downloadContent = responseEntity.getBody();  
// 大文件  
ResponseExtractor<ResponseEntity<File>> responseExtractor = new ResponseExtractor<ResponseEntity<File>>() {    
    @Override    
    public ResponseEntity<File> extractData(ClientHttpResponse response) throws IOException {        
        File rcvFile = File.createTempFile("rcvFile", "zip"); 
        FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));        
        return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(rcvFile);    
    }
};
File getFile = this.restTemplate.execute(targetUri, HttpMethod.GET, null, responseExtractor);

Service注入

@Servicepublic 
class DeviceService {
    @Resource    
    private RestTemplate restTemplate;
}

实际使用例子

// 开始推送消息logger.info("解绑成功后推送消息给对应的POS机");
LoginParam loginParam = new LoginParam();
loginParam.setUsername(managerInfo.getUsername());
loginParam.setPassword(managerInfo.getPassword());HttpBaseResponse r = restTemplate.postForObject(        
    p.getPosapiUrlPrefix() + "/notifyLogin", loginParam, HttpBaseResponse.class);
if (r.isSuccess()) {    
    logger.info("推送消息登录认证成功");
    String token = (String) r.getData(); 
    UnbindParam unbindParam = new UnbindParam();  
    unbindParam.setImei(pos.getImei());   
    unbindParam.setLocation(location);
    // 设置HTTP Header信息   
    URI uri;    
    try {       
        uri = new URI(p.getPosapiUrlPrefix() + "/notify/unbind");  
    } catch (URISyntaxException e) {   
        logger.error("URI构建失败", e);    
        return 1;    
    }  
    RequestEntity<UnbindParam> requestEntity = RequestEntity            .post(uri)  
        .contentType(MediaType.APPLICATION_JSON)     
        .accept(MediaType.APPLICATION_JSON)     
        .header("Authorization", token)     
        .body(unbindParam); 
    ResponseEntity<HttpBaseResponse> responseEntity = restTemplate.exchange(requestEntity, HttpBaseResponse.class); 
    HttpBaseResponse r2 = responseEntity.getBody();
    if (r2.isSuccess()) {      
        logger.info("推送消息解绑网点成功"); 
    } else {     
        logger.error("推送消息解绑网点失败,errmsg = " + r2.getMsg());  
    }
} else { 
    logger.error("推送消息登录认证失败");
}

本文地址:
SpringBoot实现RESTful与RestTemplate

推荐

JAVA自定义注解

上一篇下一篇

猜你喜欢

热点阅读