Build RESTful API in Java with Spring Boot

in #programming3 years ago

I am fond of programming languages — but I am not a fan of Java: I do prefer C, C++, Go, Python, Ruby, Ada, Raku, Erlang, Haskell… But Java is very much popular and cosidered an “important” and “strong” language.

In this article I will briefly explain why I am not a fan, and then I will show you how to use it to quickly build a RESTful server using Spring Boot, which is kind of cool.

First thing first: hi to everybody

Long time no see! I know I've discontinued at least two big things:

I can't promise I'll have the time and energy to complete them, some day, but I'd like to. And also I'd like to add a series about electronics (especially digital) and chip design in VHDL or Verilog… hard topic I am dreaming to learn about in sufficient depth so to design a basic CPU…

Enough collateral talking: let's see the main topic of this article.

What do you need

This is not a lesson about Java. Therefore I assume you know it a little bit already. If you don't, you have to find tutorials online and learn at least the basic. Prior programming experience, even with other programming languages, could help.

About the software and tools, you'll need

Now that you've all set up, why I don't like Java?

To me, Java is a defective language. You can do incredible things also in a defective language. Men landed on the Moon, and the Apollo Guidance Computer was programmed in assembly, and the hardware was, well, maybe the best at the time, and yet by today-standards it's unthinkable to go to the Moon with that. When compared to other languages we have today, Java is defective, and sometimes by design.

No unsigned integer numbers

Java doesn't have unsigned integers, by design — not all programmers know how to handle unsigned integers correctly, Gosling said — hence, let's remove unsigned integers!

    byte[] header = {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};

This is how you want to write the canonical PNG header. But Java says no:

error: incompatible types: possible lossy conversion from int to byte

This is because 0x89, a number greater than 127, is interpreted as a signed integer (a 32bit value), but at the end it must fit into a (signed) byte… hence the lossy conversion from int to byte error. An unsigned byte can hold numbers between 0 and 255, a signed byte can hold numbers between 0 and 127 only. Any solution that avoids this problem is a horrible patch.

Objects aren't values but references, and no “contracts” between callers and callees

Java hides the fact that objects aren't values, but always and only references, that is, pointers (that's why you can see the Null pointer exception): your variable myClassInstance doesn't hold the object, but a reference to the object.

Note that the fact that internally objects need to be held as pointers is an implementation detail and has nothing to do with how the language “presents” the concepts of variables and objects to the users and how it handles them for the user. Java “presents” them in such a way that programmers might forget it and think that they are passing objects around as values.

But it isn't so and it has important consequences: when you pass an object (an instance of a class) to a method, this method has the reference to the actual object, and so it can alter your object, for instance by calling on it a method which changes its state. There is no easy way to avoid this.

In other languages you might have objects as values. This, however, may imply a copy, which perhaps you don't want. So sometimes you can pass also by reference, but there must be also a way to “protect” your referenced objects by establishing a “contract” between the caller and the callee — the compiler will check that the contract is fulfilled.

For instance, in C++ a function which promises not to alter its reference-to-an-object parameter has the keyword const:

    void i_do_not_change_it(const MyThing& obj);

In the class MyThing, methods which do not change the state of the object are marked as const method. The compiler check that nobody lies, and you are on the road to make better, more robust, less buggy software.

Other languages share the same weakness as Java; when they are less known, or less used, or used usually in small projects, then they can be forgiven. But Java is used a lot in many big projects; often your Java project has tons of dependencies… therefore you strongly feel the need for this kind of “assurance”.

Imports can't be renamed

Recently I had a @RequestBody annotation on a method's parameter. Everything's looked fine and compiled fine. But it didn't work. The method should have received the body of the request (talking about REST services which we'll see later on), but it was empty instead. As if the framework didn't pass it. It turned out that the same annotation @RequestBody exists in another package used in the same file (the import was already added automatically by Eclipse), but the one I needed was the other one, that is, I needed

import org.springframework.web.bind.annotation.RequestBody;

But I had this one already:

import io.swagger.v3.oas.annotations.parameters.RequestBody;

In Java you can't rename those imports. This mean that in cases like this when you need to use both, at least one of them needs to be written with all the “path”. That is, if you import io.swagger..., then you must use

@org.springframework.web.bind.annotation.RequestBody;

everywhere you need the other one. And viceversa.

Other JVM languages (e.g., Groovy, Kotlin…) allow renaming/aliasing. E.g., in Kotlin:

import io.swagger.v3.oas.annotations.parameters.RequestBody as SwaggerBody
import org.springframework.web.bind.annotation.RequestBody

And then you can use @RequestBody and @SwaggerBody in the rest of the file.

There can be a toxic culture that says that renaming isn't good:

I have seen "import renaming" at work with Eiffel and it’s not pretty.
It’s a good example of a feature that sounds good in theory but ends up making your code very hard to read and to maintain.

See the parallel: Java hasn't unsigned integer numbers because Gosling thought that there are programmers who might handle unsignedness improperly. And this guy didn't like Groovy to have the renaming feature because, according to him, it can make the code hard to read and to maintain. But the code doesn't write itself: programmers do. Therefore what he's really saying is this: programmers (those who can't get signed/unsigned integers correctly, nor are able to learn more about it by experience and usage) are bad… so they might misuse the feature — hence you must not give this power to programmers!

As said, this is a toxic culture: instead of pushing forward programmers to make them better, you remove sharp corners and knives so that they can't hurt themselves or others. Java is built on this toxic culture. (In the case of Groovy, whoever was in charge of language definition and evolution didn't listen to this guy and similar voices, luckly: Groovy has the renaming feature.)

You can't overload operators

This is another silly limitation by design. I don't know if Gosling explained why, but because of his explanation for the missing unsigned integer types, I can imagine him saying: “programmers can get so confused by overloading of operators…” or “programmers are bad and stupid, and would go overloading mindlessly, making the code a mess!”

Note that you can't overload operators, but Java has overloaded operators! E.g., "String" + "Concatenation" and 45 + 10 show clearly that + is overloaded.

Class hasn't properties

This summarises to this fact: you have to write getters and setters. An IDE like Eclipse helps you generating getters and setters, but these often are trivial and pollute your code: you don't want to see

public Type getVarType() {
    return this.varType;
}

public void setVarType(Type varType) {
    this.varType = varType;
}

A fix to the problem is the use of Lombok… which tries to fill several holes in the language.

A REST service using Spring Boot

Now that you know three or four of the things that annoy me in Java, let's see how easy is to make a running REST-like HTTP service in Spring Boot.

Spring Boot is “just” Spring but with a lot of magic for autoconfiguration and alike, so that you write less and can boot your app in “no time”.

Spring Boot initializer

Go to the initializr (no typos here), which is an online tool that makes an archive you can use as a starting point for your project (once unarchived). This tool can be integrated with Eclipse (search Spring Boot in the market place), but you don't need to.

Let's use Gradle, Java as the language (despite its defective design), Spring Boot version 2.5.0, packaging jar and Java 11 (you just installed JDK 16, so that you could select Java 16 too… anyway, we don't need recent language features).

Click Add dependencies, find and select Spring Web.

01-01-springboot-initzr.png

If you want to use Lombok (which I'd recommend) you need to add Lombok too, and configure your IDE to process annotations, otherwise it won't see methods generated by Lombok. On the Lombok site you find everything you need. However, for this demo I'll go with the plain Java way.

Hit the GENERATE button at the bottom to download mapper.zip. Unzip it wherever you prefer. Now, you need to import this Gradle project in your IDE. I'm using Eclipse and have a Gradle-related plugin installed from the market place, so I do Import… → Gradle → Existing Project…, and so on (this isn't a lesson about how to import Java projects into Eclipse…). At the end I have the project ready, and it appears like this in the Eclipse's model explorer:

01-02-eclipse-model-explorer.png

You see that there are things like src/test/java. We are not going to do any testing — we don't even included JUnit among the dependencies, that is, tests won't compile! Therefore, I will simply delete anything tests related. This is not good practice: unit tests are somewhat important, but here we're not concerned about it.

In the build.gradle file you read something like:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

Well, just remove some stuff, and we are left with the following in the dependencies part of the build.gradle file:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

Now we have already MapperApplication.java, but we need a controller — that is, the part which actually contains the code to handle the requests (Indeed, most of the actual code should be in a service layer, but here we don't have too much to do, so… let's keep it simple). So, let's create it in the same package xinta.examples.mapper. Also, it is a controller for a REST like API; so we mark it with the @RestController Spring annotation.

Now, we want to make a simple REST service with three operations: insert a new key-value element; get a value from a key; delete a key-value element. We don't have any kind of persistence layer, that is, if we stop the server, we lose every key-value association.

The key is just a string, and the value holds data like this (represented in JSON):

{
  "name": "Xinta",
  "surname": "Antix",
  "appendices": 5
}

We use JSON because in RESTful HTTP services it is usually used JSON. JSON is lighter than XML and integrates better with the web (JSON stands for JavaScript Object Notation); XML is more verbose: we leave it to SOAP and others.

The JSON above is just a representation of the information we want to associate to a key. But internally, we want it as an object; that is, we need a class; let's call it Value, and to avoid the getter/setter pollution problem, let's create another potential problem by making the fields public:

public class Value {
    public String name;
    public String surname;
    public int appendices;
}

Not a big deal in our case, but however it isn't the way it is usually done… That is, usually you have private (or maybe protected) fields, and public trivial getters/setters.

The serialization/deserialization is done automagically by fasterxml jackson library implied by the Spring Boot web dependency: we don't need to worry about it, but we need to mark the class with @RestController, which implies a @ResponseBody on each methods, among other things.

By default, field variable names and JSON key names match. If we want something different, we need to add an annotation, like this:

    @JsonProperty(value = "appendices")
    public int numberOfAppendices;

Now, let's quickly define what's inside the controller:

  1. insert a new key-value
  2. remove a key-value (by using its key)
  3. get a value by key

Each of these functions is a method that will be annotated so that Spring knows when to call it, and with which parameters. Our REST APIs can be as the following:

  1. insert a new key-value → POST /key/{key}, with the proper JSON as body of the request
  2. remove a key-value → DELETE /key/{key}
  3. get a value by key → GET /key/{key}

When you see XXX /yyy/{zzz}, you can read it like this: this is an HTTP request with method XXX and the final part of the URL is /yyy/{zzz}, where {zzz} must be replaced by a value. (It also needs to be properly URL-encoded, if necessary.)

Now, maybe you need a little bit of terminology about HTTP requests and their format. Try to read this overview of HTTP and on wikipedia. The most used methods in HTTP requests are GET, POST, PUT. The GET method is the one that your browser uses the most, because it is how it retrieves HTML pages and related resources (CSS stylesheets, images, Javascript code, …)

What is actually done when a GET request is sent depends on the server receiving it. Web servers act like, well, web servers… and the WWW counts on that. Your servers might do whatever… but usually it is a good idea to stick to a meaningful semantic; that is, a GET method will GET something, and not delete a resource…

Let's implement these methods:

  • void insertValue(String key, Value value);
  • void deleteValue(String key);
  • Value getValue(String key);

We can put these into an interface and then document them using annotations that will make it possible to have an OpenAPI specification directly from the code. But I won't show how here. Let's just use an interface to underline what's exposed in our controller.

Now the question is: how do I tell Spring the endpoint and from where to get key and value? The endpoint is the /key/something path — indeed the complete endpoint has also the protocol, e.g. http://, the host, e.g. localhost and the port, e.g. :8080 if we are not using the default port for the web, which is 80 without TLS, that is, for http:// — but to run a server on ports lower than 1025 you need to be a privileged user (root/administrator), and that's why local servers often use 8080 as default.

The answer to the question is: of course, we use the proper annotations!

    @GetMapping(value = "/key/{key}", produces = MediaType.APPLICATION_JSON_VALUE)
    public Value getValue(@PathVariable String key) {
        // ...
    }

This means: when a GET request arrives on the server and the path is /key/something, call this method passing something as the key, and expect a Value instance in return (here there should be the @ResponseBody annotation I've talked about above, telling Spring that the response body must be produced from the returned object — but if you annotate the class with @RestController, you can omit it).

You can see that {key} is considered a path variable and tells which Java variable gets the value something from the path. The name appearing in {key} must match the name of the variable marked by the annotation @PathVariable (but you can specify a different path variable adding and argument to the annotation), that is, key. This magic trick is done using reflection.

The method must return a Value object, but in the world outside there's no such a thing like “a Java object”. We need to serialize the object into a format which allows us to convey its content to the caller. This operation sometimes is called by other names than serialization, e.g. marshalling but I'm going to use serialization.

The produces = MediaType.APPLICATION_JSON_VALUE tells Spring that we want the object to be serialized as JSON. It also will set a proper HTTP header (Content-Type: application/json) so that the caller knows what's receiving, too.

By default, the HTTP return code will be 200, that is ok. We can say the default return code of our method must be something different by using @ResponseStatus annotation. Codes like 2xx are used for different kind of ok, but the most common are 200 (ok) and 201 (ok, created), which seems a good fit for when we insert a new key-value.

The insert method would look like this:

    @PostMapping(value = "/key/{key}", consumes = MediaType.APPLICATION_JSON_VALUE)
    @ResponseStatus(value = HttpStatus.CREATED)
    public void insertValue(@PathVariable String key, @RequestBody Value value) {
        // ...
    }

Instead of produces, here we have consumes; it's because this API takes a JSON as input; Spring must know it is a JSON, so that it can deserialize it into a Java object (an instance of Value) and pass it to the method (second argument, in this case).

When we add a key-value, we return 201, which is the Ok - created I talked above. This is set by the @ResponseStatus annotation.

You can see that the path is the same as the one for the GET. In fact, the only difference between the two is the HTTP method used.

The delete API is similar, but it doesn't consume an input, nor produces an output. So it is like this:

    @DeleteMapping(value = "/key/{key}")
    public void deleteValue(@PathVariable("key") String keyToBeDeleted) {
    // ...
}

As an example, I've used keyToBeDeleted instead of just key; then, I had to add an argoment to the annotation to tell Spring from which path variable it can get the value to assign to keyToBeDeleted.

Now we have the skeletons of our 3 APIs. But what happens if users try to get a value for a key which doesn't exist? We need to return an error code. HTTP has 404 for Not Found (see other HTTP codes. I am sure you've seen the 404 sometimes while browsing the WWW, even if usually sites “hide” the low level details by showing you a proper 404 web page.

If we've marked getValue as returning a Value and status 200, how can we signal this exceptional case when the key doesn't exist? By using exceptions, of course!

We create a new file, KeyNotFound.java, which will be an exception we will throw when we can't find the key.

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason="key not found")
public class KeyNotFound extends RuntimeException {
    private static final long serialVersionUID = -8071011625815613946L;
}

This is all the code of the exception.

Now we have to fill the code in the Java methods, and test our APIs.

For the full code, see this git repository.

Now that it's all done, let's open a command line, cd into the project folder, and do

gradle bootRun

Some guys like to do everything from the IDE. But everything would be different in different IDEs, and I can't cover all the cases but the Eclipse one. Indeed, I don't like to do everything from the IDE. Gradle can be called from Eclipse, but it exists even without Eclipse running or other IDEs. So, learn how to open a terminal, console, command line… whatever you call it in your operating system, learn how to move from a folder (or directory) to another, and so on.

Write the command gradle bootRun and if you have gradle properly set up… You'll see

01-03-springbootstart.png

The important things to notice here are: the HTTP server is listening on port 8080; our application is “bound” to a null context. This means that we can reach our APIs using an address like this:

http://localhost:8080/key/123

Were the context mapper, we would have

http://localhost:8080/mapper/key/123

We can configure Spring Boot so that Tomcat listens on other ports, and we can define a context, which is important if we deploy the application in a container where other applications live.

Using the API

The server we made needs a container it can run in; the default embedded container for Spring Boot is Tomcat. We could have chosen war as package, and the produced war could have been deployed in, say, JBoss, or other application servers.

Now we need to make HTTP calls to our server. We can use curl, but also tools like postman or even a browser (the GET comes easy; for the POST and DELETE, you need to program at least a HTML form, and maybe javascript code).

With curl, let's try

curl localhost:8080/

The server (Tomcat) answer

{"timestamp":"2021-06-06T14:24:39.955+00:00","status":404,"error":"Not Found","path":"/"}

That's because we haven't provided methods to handle requests on /. The Tomcat server doesn't know what to do with the request, where to dispatch it to. And so it returns 404 and the JSON object you see.

To see the return code, add the -v option like this:

curl -v localhost:8080

The output (shortned) will be:

* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.65.3
> Accept: */*
...
< HTTP/1.1 404 
...
< Content-Type: application/json
...

By default curl makes a GET request. If you try

curl -v localhost:8080/as/you/can/see/this/path/is/long

you'll see:

...
> GET /as/you/can/see/this/path/is/long HTTP/1.1
> Host: localhost:8080
...

Our path must be /key/1234 or alike. Let's try to add a value (the $ mark the prompt of the command line, followed by the command I gave, and then its output (sortned):

$ curl -v localhost:8080/key/1234 -H 'Content-Type: application/json' -d '{}'
*   Trying 127.0.0.1:8080...
...
> POST /key/1234 HTTP/1.1
> Host: localhost:8080
...
> Content-Type: application/json
...
< HTTP/1.1 201 

Response 201! It means we have inserted the key-value. But where's the value? It's an empty JSON object, that is, {}: the -d option is used to pass a body to a request (and automatically makes curl use the POST method, because it is the POST method that usually has a body).

Let's try to get the key 1234:

$ curl localhost:8080/key/1234
{"name":null,"surname":null,"appendices":0}

We created a key-value, but the value was empty. We didn't say it must not be empty, and so it was accepted. The Java Value instance has nulls for Strings and 0 for appendices.

There are ways to validate the input (again, using annotations), so that it must respects constraints or an error is returned. We didn't use this possibility, nor we did a direct check in code.

Now let's try again to add the key 1234 with name:

$ curl -v localhost:8080/key/1234 -H 'Content-Type: application/json' -d '{"name":"Xinta"}'
...
< HTTP/1.1 409 

The response code is 409, that is, HttpStatus.CONFLICT. It is what we've specified in the exception, using @ResponseStatus. If we need a custom response body, we need a more custom error handling, maybe we need to tap into some mechanisms of Spring. We won't do that in this article.

To modify our key 1234, we must delete it first. This is how the current code works, not how thing must be done!

$ curl -X DELETE localhost:8080/key/1234
$ curl localhost:8080/key/1234 -H 'Content-Type: application/json' -d '{"name":"Xinta"}'
$ curl localhost:8080/key/1234
{"name":"Xinta","surname":null,"appendices":0}

Just a final note: if we omit the -H 'Content-Type: application/json' option, which makes curl add the Content-Type header to the request, we are omitting the “type” of body. Since the server don't know how to deserialize unknown “types”, it gives an error:

{
   "error" : "Unsupported Media Type",
   "timestamp" : "2021-06-06T14:54:50.571+00:00",
   "path" : "/key/5678",
   "status" : 415
}

(This output was reformatted by json_pp)

Final notes

Now you should be able to make and run simple HTTP REST services accepting HTTP requests. But if you want to make something useful, maybe you need to add “persistence”, that is, something used by the application to store values permanently, and to retrieve them, so that they don't disappear when you stop the application.

Now it seems like we did an incredible thing with no efforts. Is it so? If it is, it's only because there's a lot of hard works made from other people.

In the future I want to show you how easy is to accomplish the same goal in other languages and framework when they have already the gears. The more a language is used (and backed by strong corporations…), the more likely you find plenty of tools and frameworks and everything you need to build quickly and easily an application.

Remember that this doesn't give a certificate of “good programming language”. Rather, it is a measure of how much money was poured and circulate inside its ecosystem.



If you spot errors, or have questions, please don't hesitate to write a comment.

Sort:  

You've got a free upvote from witness fuli.
Peace & Love!