How do you design a web application in Java? You install Spring, read the manual, create controllers, create some views, add some annotations, and it works. What would you do if there were no Spring (and no Ruby on Rails in Ruby, and no Symphony in PHP, and no … etc.)? Let’s try to create a web application from scratch, starting from a pure Java SDK and ending with a fully functional web app, covered by unit tests. I recorded a webinar no.42 about it just a few weeks ago, but this article should explain it all in even more detail.
First of all we have to create an HTTP server, which will open a server socket, listen to incoming connections, read everything they have to say (HTTP requests) and return the information any web browser would like (HTTP responses). You know how HTTP works, right? If you don’t, here is a quick reminder:
A web browser sends a request to the server and the request looks like this (it’s a plain text piece of data):
GET /index.html HTTP/1.1
Host: www.example.com
The server has to read this text, prepare the answer (which has to be the HTML page readable by the browser) and return it like this:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 26
<html>Hello, world!</html>
That’s it. It’s a very simple and, I would say, primitive protocol. The implementation of a web server in Java is not so complex either. Here it is, in a very simplistic form:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Arrays;
public class Main {
public static void main(String... argv) {
try (ServerSocket server = new ServerSocket(8080)) {
server.setSoTimeout(1000);
while (true) {
try (Socket socket = server.accept()) {
try (InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream()) {
byte[] buffer = new byte[10000];
int total = input.read(buffer);
String request = new String(Arrays.copyOfRange(buffer, 0, total));
String response = "HTTP/1.1 200 OK\r\n\r\nHello, world!";
output.write(response.getBytes());
}
} catch (SocketTimeoutException ex) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
}
}
}
}
Try to run it, it should work. You should be able to open the http://localhost:8080
page in your browser and see the Hello, world!
text.
It’s not yet a web app, but just a skeleton, which does simple dispatching of HTTP requests into HTTP responses. There is no serious OOP in it though. It’s pretty procedural, but it works. Now we should focus on a more important question: How do we add more features to the web app and make it possible to process different pages, render larger content, and handle errors? The request
variable in the snippet above should be somehow converted to a response
.
The easiest way would be 1) to convert the request into a DTO with all the details inside, then 2) send it to a “controller” that knows what to do with the data from the DTO, and then 3) receive a response DTO from the controller, take the data out and render the response. This is how Spring and most all other frameworks do it. However, we won’t follow this path, we will try to do it DTO-free and purely object-oriented.
I have to say that there could be multiple designs, all in an OOP style. I’ll show you now only one of those options. You’re no doubt aware of our Takes framework, which was born a few years ago—it has its own design, also object-oriented. But the one I’m going to suggest now seems to be better. You might come up with something else too, so don’t hesitate to post your ideas in the comments below or even create a GitHub repo and share your thoughts right there.
I suggest we introduce two interfaces: Resource
and Output
. The Resource
is the server side entity, which mutates depending on the request parameters that are coming in. For example, when all we know about the request is that it is GET /
, it is one resource. But if we also know that the request has, for example, Accept: text/plain
, we can mutate the request and create a new one, which delivers plain text. Here is the interface:
interface Resource {
Resource refine(String name, String value);
}
Here is how we create it and mutate:
Resource r = new DefaultResource()
.refine("X-Method", "GET")
.refine("X-Query", "/")
.refine("Accept", "text/plain");
Pay attention: each call to .refine()
returns a new instance of interface Resource
. All of them are immutable, just like objects have to be. Thanks to this design we don’t separate data from their processor. The resource is the data and the processor. Each resource knows what to do with the data, and receives only the data it is supposed to receive. Technically, we just implement request dispatching, but in an object-oriented way.
Then, we need to convert the resource to the response. We give the resource the ability to render itself to the response. We don’t want the data, in form of some DTO, to escape the resource. We want the resource to print the response. How about giving an additional method print()
to the resource:
interface Resource {
Resource refine(String name, String value);
void print(Output output);
}
And then the interface Output
looks like this:
interface Output {
void print(String name, String value);
}
Here is a primitive implementation of Output
:
public class StringBuilderOutput implements Output {
private final StringBuilder buffer;
StringBuilderOutput(StringBuilder buf) {
this.buffer = buf;
}
@Override
public void print(String name, String value) {
if (this.buffer.length() == 0) {
this.buffer.append("HTTP/1.1 200 OK\r\n");
}
if (name.equals("X-Body")) {
this.buffer.append("\r\n").append(value);
} else {
this.buffer.append(name).append(": ").append(value).append("\r\n");
}
}
}
To build an HTTP response we can do this:
StringBuilder builder = new StringBuilder();
Output output = new StringBuilderOutput(builder);
output.print("Content-Type", "text/plain");
output.print("Content-Length", "13");
output.print("X-Body", "Hello, world!");
System.out.println(builder.toString());
Now let’s create a class which will take an incoming request String
and produce a response String
, using an instance of Resource
as a dispatcher:
public class Session {
private final Resource resource;
Session(Resource res) {
this.resource = res;
}
String response(String request) throws IOException {
Map<String, String> pairs = new HashMap<>();
String[] lines = request.split("\r\n");
for (int idx = 1; idx < lines.length; ++idx) {
String[] parts = lines[idx].split(":");
pairs.put(parts[0].trim(), parts[1].trim());
if (lines[idx].empty()) {
break;
}
}
String[] parts = lines[0].split(" ");
pairs.put("X-Method", parts[0]);
pairs.put("X-Query", parts[1]);
pairs.put("X-Protocol", parts[2]);
App.Resource res = this.resource;
for (Map.Entry<String, String> pair : pairs.entrySet()) {
res = res.refine(pair.getKey(), pair.getValue());
}
StringBuilder buf = new StringBuilder();
res.print(new StringBuilderOutput(buf));
return buf.toString();
}
}
First, we parse the request, breaking its header into lines and ignoring the body of the request. You can modify the code to parse the body and pass it into the refine()
method too, using X-Body
as the key. At the moment, the code above doesn’t do that. But you get the idea. The parsing part of the snippet prepares the pairs it can find in the request and passes them one by one to the encapsulated resource, mutating it until it gets to the final form. A simple resource that always returns text might look like this:
class TextResource implements Resource {
private final String body;
public TextResource(String text) {
this.body = text;
}
@Override
public Resource refine(String name, String value) {
return this;
}
@Override
public void print(Output output) {
output.print("Content-Type", "text/plain");
output.print("Content-Length", Integer.toString(this.body.length()));
output.print("X-Body", this.body);
}
}
A resource that pays attention to the query string and dispatches the request to other resources, depending on the path in the query, might look like this:
new Resource() {
@Override
public Resource refine(String name, String value) {
if (name.equals("X-Query")) {
if (value.equals("/")) {
return new TextResource("Hello, world!");
} else if (value.equals("/balance")) {
return new TextResource("256");
} else if (value.equals("/id")) {
return new TextResource("yegor");
} else {
return new TextResource("Not found!");
}
} else {
return this;
}
}
@Override
public void print(final Output output) {
throws IllegalStateException("This shouldn't happen");
}
}
I hope you got the idea. The code above is rather sketchy, and the majority of use cases are not implemented, but you can do that yourself, if you are interested. The code is in the yegor256/jpages repository. Don’t hesitate to contribute with a pull request and make this small framework real.