JDK6 представил отличную, но, вероятно, не слишком широко известную возможность встраивания облегченного http-сервера в ваше приложение. Если вы не знаете об этом, как я здесь, это ссылка на javadocs простого и приятного API. Конечно, раньше существовало несколько отличных вариантов для встроенного http-сервера (обычно сервлет-контейнера). Причал долгое время был моим любимым. Самым большим отличием является то, что JDK дает вам нечто действительно минималистичное, но чрезвычайно простое и легкое. Конечно, это не хорошо в качестве замены производственного сервера приложений, но просто идеально подходит, например, для удаленного управления приложением.
Мой личный случай использования следующий: у меня есть несколько машин, работающих на EC2. Вместо того, чтобы через терминал использовать десятки различных сценариев оболочки, у меня есть один скрипт Groovy, который запускает простой веб-сервер на каждой машине. Все остальное делается через окно браузера.
В этой статье я объясню, как я обертываю API-интерфейс http-сервера в Groovy ++ DSL
Давайте начнем с самого простого из возможных «Привет, мир!» сервер.
def logger = Logger.getLogger(this.class.name)
httpServer(8081) { exchange ->
exchange.sendResponseHeaders(200,0)
exchange.responseBody.withPrintWriter { out ->
out.println "Hello, World!"
}
logger.info """URI: ${exchange.requestURI}
${exchange.requestHeaders}"""
}
То, что делает сервер выше, это ответ с текстом «Hello, World!» для любого запроса и регистрирует запрос URI и заголовки. Очень простой. Вероятно, слишком просто даже для самого простого реального применения. Но прежде чем перейти к чему-то более реальному, давайте посмотрим, что делает метод httpServer .
static HttpServer httpServer(int port = 8080, HttpHandler handler) {
httpServer(port) { ->
context("/", handler)
}
}
Здесь мы видим, что метод принимает два параметра: номер порта и экземпляр класса, реализующего интерфейс HttpHandler из JDK.
HttpHandler — это так называемый класс Single Abstract Method (SAM). Это позволяет Groovy ++ компилятору без проблем создавать его из выражения замыкания. Мы будем использовать это наблюдение несколько раз в этой статье.
Как мы видим, реализация метода httpServer вызывает другой метод с тем же именем, но с разными аргументами.
static HttpServer httpServer(int port = 8080, ServerDefinition config) {
def server = HttpServer.create(new InetSocketAddress(InetAddress.localHost, port), 0)
config.server = server
config.define()
server.start()
server
}
Приведенный выше метод также принимает два параметра: номер порта и ServerDefinition. ServerDefinition — это SAM, который немедленно вызывается после создания сервера и должен конфигурировать все необходимые параметры (контексты http с соответствующими обработчиками, как глобальные, так и для контекстных фильтров и аутентификатор)
Внимательный читатель может быть заинтригован в этот момент, как компилятор решает, представляет ли выражение закрытия ServerDefinition или HttpHandler
На самом деле это очень просто: по параметрам выражения замыкания. Единственный абстрактный метод HttpHandler имеет один параметр типа HttpExchange, а единственный абстрактный метод ServerDefinition не имеет параметров.
Вот это же «Hello, World!» Пример переписан в более канонической форме. Мы определяем глобальный фильтр, который будет применяться ко всем http-контекстам на нашем сервере, и единственный http-контекст на нашем сервере, который отвечает «Hello, World!» на любой запрос.
httpServer(8082) { ->
filter { exchange, chain ->
logger.info """URI: ${exchange.requestURI}
${exchange.requestHeaders}"""
}
context("/") { exchange ->
exchange.sendResponseHeaders(200,0)
exchange.responseBody.withPrintWriter { out ->
out.println "Hello, World!"
}
}
}
Теперь мы почти готовы реализовать класс ServerDefinition. Но перед этим давайте создадим общий суперкласс для ServerDefinition и ContexDefinition (мы поговорим об этом чуть позже) и специализированной реализации Filter.
abstract class DefaultFilter extends Filter {
String description = "Filter ${this.class.name}"
String description () {
description
}
}
abstract class FiltersDefinition {
protected List<Filter> filters = []
void filter (String description = null, DefaultFilter filter) {
if (description)
filter.description = description
filters << filter
}
}
Зачем нам нужна специализированная реализация Filter?
Фильтр не SAM, поэтому мы не можем использовать закрывающее выражение, когда нужно его реализовать. Обычно [:] синтаксис Groovy ++ выполняет эту работу, но в нашем конкретном случае возможны более элегантные решения, описанные выше.
Теперь давайте посмотрим на реализацию ServerDefinition
abstract class ServerDefinition extends FiltersDefinition {
private HttpServer server
abstract void define ()
void context(String path, ContextDefinition contextConfig) {
def context = server.createContext(path)
contextConfig.context = context
context.filters.addAll (filters)
contextConfig.define()
context.filters.addAll (contextConfig.filters)
context.handler = { exchange ->
def method = exchange.requestMethod
def handler = contextConfig.methods[method]
if (handler)
handler.handle(exchange)
else {
exchange.sendResponseHeaders(404,0)
def out = new PrintStream(exchange.responseBody)
out.print "HTTP method ${method} is not supported"
out.close ()
}
exchange.close ()
}
}
void context(String context, HttpHandler handler) {
def httpContext = server.createContext(context)
httpContext.filters.addAll (filters)
httpContext.handler = { exchange ->
try {
handler.handle(exchange)
}
finally {
exchange.close ()
}
}
}
}
Мы уже видели в действии второй вариант контекста метода . Он принимает контекстный путь и HttpHandler в качестве параметров. Обработчик выполняет реальную работу, но мы обертываем его, чтобы блокировать try / finally, чтобы убедиться, что наша обработка будет завершена правильно.
Обратите внимание, как элегантно выглядит закрытие оболочки по сравнению с обычным анонимным внутренним классом Java
Первый вариант метода контекста немного сложнее. Прежде чем мы объясним, что такое ContextDefinition, вероятно, будет проще показать, как мы будем его использовать.
Следующий фрагмент кода реализует простой сервис, который позволяет клиентам помещать / получать пары ключ / значение на сервер. Ключи — это строки, а значения — любые двоичные данные. Ключи будут запрошены в форме «/ map / <key>». Для извлечения данных будет использоваться метод http GET, а для PUT — метод PUT.
context("/map/") { ->
@Field ConcurrentHashMap<String, byte[]> data = []
filter { exchange, chain ->
String key = exchange.requestURI.toString().substring(exchange.httpContext.path.length())
exchange.setAttribute("key", key)
if (key)
chain.doFilter(exchange)
else {
exchange.sendResponseHeaders(404,-1)
exchange.close ()
}
}
get { exchange ->
String key = exchange.getAttribute("key")
def result = data[key]
exchange.sendResponseHeaders(200,4 + result?.length)
exchange.responseBody.withDataOutputStream { out ->
if (result) {
out.writeInt(result.length)
out.write(result)
}
else {
out.writeInt(-1)
}
}
}
post { exchange ->
exchange.sendResponseHeaders(200,-1)
String key = exchange.getAttribute("key")
exchange.requestBody.withDataInputStream { input ->
def len = input.readInt ()
if (len < 0)
data.remove(key)
else {
def bytes = new byte [len]
input.read(bytes)
data.put(key, bytes)
}
}
}
}
Теперь я рекомендую пересмотреть первую версию метода ServerDefinition.context . Что происходит, мы выбираем обработчик на основе используемого метода http
Для завершения нашей истории нам нужно определить класс ContextDefinition. Это действительно просто.
abstract class ContextDefinition extends FiltersDefinition {
protected HttpContext context
abstract void define ()
Map<String,HttpHandler> methods = [:]
void method(String method, HttpHandler action) {
methods[method] = action
}
void get(HttpHandler action) {
method("GET", action)
}
void post(HttpHandler action) {
method("POST", action)
}
void setAuthenticator(Authenticator authenticator) {
context.setAuthenticator(authenticator)
}
}
Мы закончили, и я надеюсь, вы согласитесь, что встраивание HttpServer, поставляемого с JDK6, совсем не сложно. Если вы также хотите больше узнать о Groovy ++ DSL, я не против потратить часть моих выходных на написание этой статьи ?
До следующего раза.