среда, 21 января 2015 г.

Самый "легкий" Java - сервер

Когда я начинал работать в Java EE, серверная инфраструктура казалась мне загадочным капризным монстром. И это при том, что в программирование я пришёл из админов. Залил ear в Resin, проверь, поднялся ли? Успешно ли распаковался? Однажды мы с админами потратили полночи чтобы понять, почему после успешного вроде бы деплоя на запросы отвечает по-прежнему старая копия приложения. При таких танцах с бубном переход на GlassFish казался хорошим решением, а отказ от ejb - ещё лучшим. Кто-то из моих коллег радовался могуществу Spring, кто-то искал что-то попроще... Я писал сервлеты под tomcat, а когда делал что-то совсем маленькое - просто встраивал jetty.
Чем проще, тем лучше. Понятно, что есть нетривиальные требования, есть сложные задачи. Но признаемся себе, сколько простых ненагруженных сервисов вы реализовали используя слишком мощные и сложные инструменты? "Я привык к этому фреймфорку и не хочу терять скилл" - слышу я обычно. "Ты уйдёшь и тот, кто будет поддерживать этот монстрокод, проклянёт тебя" - думаю я в ответ.
Может я и не убедил вас писать простые вещи простыми средствами, но посмотрите все-таки как можно сделать серверное java-приложение вообще без сервера. Это, в конце концов, интересно.



Как, вообще без сервера? Писать обработку запросов самому?

Ну, нет, что вы. Я за простые решения, но не за велосипеды. Все уже написано и, более того, уже установлено. Java-сервер уже есть в вашем jdk. Просто используйте его )

import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
 
public class Main {
 
    private static final int PORT = 9090;
 
    public static void main(String[] args) throws IOException {
        int port = PORT;
        if (args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
        server.createContext("/publish", new PublishHandler());
        server.createContext("/subscribe", new SubscribeHandler());
        server.setExecutor(Executors.newCachedThreadPool());
        server.start();
        System.out.println("Server is listening on port " + port);
    }
}

com.sun.net.HttpServer - вот все что нам нужно. Наше приложение будет слушать запросы на порту 9090 или том, что будет указан java -jar наш.jar тут. Наши обработчики мы навешиваем на контексты перед стартом сервера. Все предельно просто. Чтобы в обработчиках сосредоточиться на бизнес-логике, сделаем им абстрактный предок, где порешаем все мелочи вроде вычитывания параметров зарпроса и логирования пары запрос-ответ:

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
 
public abstract class HttpHandlerBased implements HttpHandler {
 
    protected abstract String doGet(HttpExchange exchange, Map<String, String> params);
 
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        String requestMethod = exchange.getRequestMethod();
        if (requestMethod.equalsIgnoreCase("GET")) {
            String uriStr = exchange.getRequestURI().getQuery();
            String id = UUID.randomUUID().toString().replace("-", "");
            log("[req " + id + "] " + uriStr);
            Map<String, String> pars = new HashMap<String, String>();
            String[] pairs;
            if (uriStr != null && uriStr.contains("=")) {
                if (uriStr.contains("&")) pairs = uriStr.split("&");
                else pairs = new String[]{uriStr};
                for (String pair : pairs) {
                    String[] p  = pair.split("=");
                    if (p.length == 2) {
                        pars.put(p[0], p[1]);
                    }
                }
            }
            String resp = doGet(exchange, pars);
            log("[rsp " + id + "] " + resp);
            exchange.sendResponseHeaders(200, resp.length());
            OutputStream body = exchange.getResponseBody();
            body.write(resp.getBytes("UTF-8"));
            body.close();
        }
    }
 
    public static void log(String msg) {
        System.out.println(new SimpleDateFormat("HH:mm:ss:SSS dd.MM.yy").format(new Date())+" - "+msg);
    }
}

Имея "за плечами" такого предка, нам останется реализовать метод doGet, в каждом конкретном "сервлете". Метод POST я тут не обрабатываю потому что он мне не нужен. Но обработать его не сложнее, можете попробовать сами.

Итак, что же мы получаем?

Для "секретного web-сервера", по сути внутреннего API java, мы имеем очень приятные инструменты для работы с данными запроса, заголовками ответа и т.п. Все это не требует никакой установки, настройки, есть прямо "в коробке" и готово к работе. Чем не инструмент для простого web-сервиса? Конечно, когда мы проверим свою идею и задумаемся о продакшене, можно перенести свою бизнес-логику в "честные" сервлеты и использовать тот же tomcat или resin. Почему бы и нет? Но если проект не пойдёт в продакшн мы скажем себе спасибо за то, что не привлекали админов, не тратили дни на развертывание и настройку окружения, а просто взяли и сделали то что было нужно. И не более того.

7 комментариев:

  1. http://sparkjava.com/ тут как по мне намного проще
    если нужно что то поумнее - есть springboot

    ОтветитьУдалить
    Ответы
    1. ещё пару интересных java-серверов. Я не против. Но фишка описанного выше способа - в отсутствии вообще чего-либо кроме стандартной jdk.

      Удалить
  2. Интересное решение, но не очень понятно - как управлять заголовками?

    ОтветитьУдалить
  3. не проще ли сервлет создать

    ОтветитьУдалить
  4. Как Вы сервлет запустите без контейнера?

    ОтветитьУдалить
  5. Про заголовки

    exchange.getResponseHeaders().put("Content-Type", Arrays.asList("text/plain; charset=UTF-8"));

    и размер задать
    byte[] bytes = builder.toString().getBytes();
    exchange.sendResponseHeaders(200, bytes.length);

    ОтветитьУдалить