Old songs about the main thing. Java and outgoing requests
- Tutorial
( Illustration )
One of the tasks faced by 99.9% of developers is to appeal to third-party endpoints. These can be either external API or “own” microservices. Now everyone and everything is beating on microservices, yes. Getting or sending data is simple, but sometimes inventing bicycles. Can you name 5 ways to implement Java queries (with and without libraries)? No - welcome under cat. Yes? Come and compare;)
0. Intro
The task that we are going to solve is extremely simple: we need to send a GET / POST request and get an answer that comes in JSON format. In order not to write another original microservice, I will use an example that provides a set of endpoint's with some data. All code samples are maximally simplified, there will not be any crafted cases with auth tokens and headers. Only POST and GET, GET and POST, and so 5 times or so.
So let's go.
1. Built-in Java solution
It would be strange if the task could not be solved without the use of third-party libraries. Sure you may! But sad. The java.net package, namely HttpURLConnection, URL and URLEnconder.
To send a request that GET, that POST, you need to create a URL object and open a connection based on it:
final URL url = new URL("http://jsonplaceholder.typicode.com/posts?_limit=10");
final HttpURLConnection con = (HttpURLConnection) url.openConnection();
Next, you need to flavor the connection with all parameters:
con.setRequestMethod("GET");
con.setRequestProperty("Content-Type", "application/json");
con.setConnectTimeout(CONNECTION_TIMEOUT);
con.setReadTimeout(CONNECTION_TIMEOUT);
And get the InputStream, from where you already read all the data.
try (final BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
String inputLine;
final StringBuilder content = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
content.append(inputLine);
}
return content.toString();
} catch (final Exception ex) {
ex.printStackTrace();
return"";
}
And, actually, we will get such an answer (it will be the same for all the following examples, because we work with the same endpoints):
[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
},
...
{
"userId": 1,
"id": 9,
"title": "nesciunt iure omnis dolorem tempora et accusantium",
"body": "consectetur animi nesciunt iure dolore\nenim quia ad\nveniam autem ut quam aut nobis\net est aut quod aut provident voluptas autem voluptas"
},
{
"userId": 1,
"id": 10,
"title": "optio molestias id quia eum",
"body": "quo et expedita modi cum officia vel magni\ndoloribus qui repudiandae\nvero nisi sit\nquos veniam quod sed accusamus veritatis error"
}
]
In the case of a POST request, everything is a little more complicated. We want not only to get an answer, but also to transmit data. For this we need to put them there. The documentation tells us that this might work as follows:
final Map<String, String> parameters = new HashMap<>();
parameters.put("title", "foo");
parameters.put("body", "bar");
parameters.put("userId", "1");
con.setDoOutput(true);
final DataOutputStream out = new DataOutputStream(con.getOutputStream());
out.writeBytes(getParamsString(parameters));
out.flush();
out.close();
Where getParamsString is a simple method that drives a Map to a String containing key-value pairs:
publicstatic String getParamsString(final Map<String, String> params){
final StringBuilder result = new StringBuilder();
params.forEach((name, value) -> {
try {
result.append(URLEncoder.encode(name, "UTF-8"));
result.append('=');
result.append(URLEncoder.encode(value, "UTF-8"));
result.append('&');
} catch (final UnsupportedEncodingException e) {
e.printStackTrace();
}
});
final String resultString = result.toString();
return !resultString.isEmpty()
? resultString.substring(0, resultString.length() - 1)
: resultString;
}
Upon successful creation, we get the object back:
{ "title": "foo", "body": "bar", "userId": "1", "id": 101}
A reference to the source, which can be run.
2. Apache HttpClient
Leaving aside the embedded solutions, the first thing we stumble upon is the Apache HttpClient. For access, we need the JAR file itself. Or, since I use Maven, the corresponding dependency is:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
And the way the lookups look like using HttpClient is much better ( source ):
final CloseableHttpClient httpclient = HttpClients.createDefault();
final HttpUriRequest httpGet = new HttpGet("http://jsonplaceholder.typicode.com/posts?_limit=10");
try (
CloseableHttpResponse response1 = httpclient.execute(httpGet)
){
final HttpEntity entity1 = response1.getEntity();
System.out.println(EntityUtils.toString(entity1));
}
final HttpPost httpPost = new HttpPost("http://jsonplaceholder.typicode.com/posts");
final List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("title", "foo"));
params.add(new BasicNameValuePair("body", "bar"));
params.add(new BasicNameValuePair("userId", "1"));
httpPost.setEntity(new UrlEncodedFormEntity(params));
try (
CloseableHttpResponse response2 = httpclient.execute(httpPost)
){
final HttpEntity entity2 = response2.getEntity();
System.out.println(EntityUtils.toString(entity2));
}
httpclient.close();
We received the same data, but wrote at the same time half the code. I wonder where else can search in such a seemingly basic question? But Apache has another module that solves our problem.
3. Apache Fluent API
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
<version>4.5.6</version>
</dependency>
And already using Fluent Api, our calls become much more readable ( source ):
final Content getResult = Request.Get("http://jsonplaceholder.typicode.com/posts?_limit=10")
.execute().returnContent();
System.out.println(getResult.asString());
final Collection<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("title", "foo"));
params.add(new BasicNameValuePair("body", "bar"));
params.add(new BasicNameValuePair("userId", "1"));
final Content postResultForm = Request.Post("http://jsonplaceholder.typicode.com/posts")
.bodyForm(params, Charset.defaultCharset())
.execute().returnContent();
System.out.println(postResultForm.asString());
And as a bonus - an example. If we want to transfer data to the body, not as a form, but as everyone's favorite JSON:
final Content postResult = Request.Post("http://jsonplaceholder.typicode.com/posts")
.bodyString("{\"title\": \"foo\",\"body\":\"bar\",\"userId\": 1}", ContentType.APPLICATION_JSON)
.execute().returnContent();
System.out.println(postResult.asString());
In fact, the calls collapsed into one line of code. As for me, it is much more friendly towards developers than the very first method.
4. Spring RestTemplate
What next? Further experience brought me to the world of Spring. And, not surprisingly, the spring also has the tools to solve our simple task (strange, true? The task, even not so - the need! - a basic level, and for some reason more than one solution). And the very first solution (basic) that you find in the Spring ecosystem is RestTemplate. And for this we need to pull a considerable part of the entire zoo. So if you need to send a request in a non-pending application, then for the sake of this it is better not to pull the whole kitchen. And if the spring is already there, then why not? How to attract all that is needed for this can be found here . Well, the actual GET request using RestTemplate looks like this:
final RestTemplate restTemplate = new RestTemplate();
final String stringPosts = restTemplate.getForObject("http://jsonplaceholder.typicode.com/posts?_limit=10", String.class);
System.out.println(stringPosts);
Gud BUT! You no longer want to work with a string, all the more there is an opportunity to receive not strings, but ready-made objects that we expect to receive! Create a Post object:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Builder@Getter@Setter@JsonIgnoreProperties(ignoreUnknown = true)
publicclassPost{
privateint id;
private String title;
private String body;
privateint userId;
public String toString(){
return String.format("\n id: %s \n title: %s \n body: %s \n userId: %s \n", id, title, body, userId);
}
}
Here:
Builder, Getter, Setter - sugar from Lombok, not to write everything by hand. Yes, here it is, laziness, mother.
JsonIgnoreProperties - so that in case of receiving unknown fields, not to fly into an error, but to use those fields that are known to us.
Well, toString to output our objects to the console, and it could be read. Well, actually our GET and POST requests are reincarnated into ( source ):
// Map it to list of objectsfinal Post[] posts = restTemplate.getForObject("http://jsonplaceholder.typicode.com/posts?_limit=10", Post[].class);
for (final Post post : posts) {
System.out.println(post);
}
final Post postToInsert = Post.builder()
.body("bar")
.title("foo")
.userId(1)
.build();
final Post insertedPost = restTemplate.postForObject("http://jsonplaceholder.typicode.com/posts", postToInsert, Post.class);
System.out.println(insertedPost);
And we already have objects in our hands, and not a string that we have to disassemble ourselves.
Cool Now we can write some wrapper around the RestTemplate so that the request is built correctly. It doesn’t look so bad, but as for me, it can still be improved. The less code is written, the less is the probability of error. Everyone knows that the main problem is often PEBCAK (Problem Exists between Chair and Keyboard) ...
5. Spring Feign
And here comes the stage of Feign, which is part of Spring Cloud. First, add a Feign dependency to the previously added spring environment:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.4.5.RELEASE</version>
</dependency>
In essence, all that is needed is to declare the interface and flavor it with good annotations. Especially this approach will be appealing to those who write controllers using a spring.
Here is what we need to do to send requests via Feign ( source ).
@FeignClient(name = "jsonplaceholder", url = "http://jsonplaceholder.typicode.com", path = "/posts")
publicinterfaceApiClient{
@RequestMapping(method = GET, value = "/", consumes = APPLICATION_JSON_VALUE)
List<Post> getPosts(@RequestParam("_limit")finalint postLimit);
@RequestMapping(method = POST, value = "/", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE)
Post savePost(@RequestBody Post post);
}
Beauty, is not it? And yes, the data models we wrote for the RestTemplate are well reused here.
6. Conclusion
There is more than one method of implementation besides the five presented. This collection is only a reflection of the author's experience in the order in which I became acquainted with them and began to use them in projects. Now I actively use Feign, enjoy life and wait for something more convenient to appear, so that you can shout “<library name>, choose you!” To the monitor - and everything is ready for use and integration. In the meantime, Feign.
PS As one of the “lazy” ways you can consider the client generated by Swagger. But as they say, there is a nuance. Not all developers use Swagger to document their APIs, and even less do it so high quality that you can easily generate and use a client and not get an entomological collection instead of it that will do more harm than good.