Статьи

Предоставление функциональности через HTTP с помощью Groovy и сверхлегких HTTP-серверов

Мне нужен был быстрый и простой способ, позволяющий некоторым пользователям запрашивать таблицу, и я понял, что самым простым решением было использование встроенного легковесного HTTP-сервера, чтобы пользователи могли вводить URL в своем браузере и получать результаты. Вопрос, конечно, был в том, какой сервер лучше для него. Я хотел бы суммировать здесь варианты, которые я обнаружил — включая Гретти, Джетти, Рестлет, Джерси и другие — и их плюсы и минусы, а также полные примеры для большинства из них. Я специально избегал различных фреймворков, которые могли бы легко это поддерживать, таких как Grails, потому что он не казался по-настоящему легким и мне требовалось только очень простое временное приложение.

Я использовал Groovy для его высокой производительности, особенно в отношении JDBC — с GSQL мне потребовалось всего две строки, чтобы получить данные из БД в удобном для пользователя формате.

Мое идеальное решение позволило бы запустить сервер с поддержкой HTTPS и авторизацией и объявить обработчики для URL программно, в одном файле (скрипт Groovy), всего в нескольких строках кода. (Очень похоже на решение Gretty ниже + материал по безопасности.)

Примечания стороны

Обратите внимание на виноград

Grape , пакетный движок Groovy, позволяет загружать зависимости во время выполнения с помощью аннотаций @Grab. Если вы запускаете свой отличный скрипт f.ex. через / bin / groovy он будет работать, потому что Groovy распространяется вместе с Ivy, что необходимо для работы Grape. (При использовании IntelliJ добавьте ivy.jar вручную в путь к классам проекта, а затем вызовите намеренное действие (Mac: Alt + Enter) в аннотации @Grab, чтобы загрузить его и добавить в путь к классам.)

Примечание по настройке HTTPS / SSL

Чтобы включить HTTPS, вам нужно будет создать хранилище ключей с парой ключей, что хорошо описано в документации Jetty (шаг 1a).

Для нетерпеливых:

  • Бег
    1
    keytool -keystore $HOME/.keystore -alias myGroovyServer -genkey -keyalg RSA
  • В ответ на вопрос «Каковы ваши имя и фамилия?» Укажите имя хоста, на котором будет работать служба, например «localhost» или «myserver.example.com».
  • Укажите один и тот же пароль для хранилища ключей и сгенерированного ключа (например, «myKeystorePsw»)
  • При запуске сервера укажите (абсолютный) путь к сгенерированному файлу .keystore (для конкретного сервера) и задайте системное свойство javax.net.ssl.keyStorePassword для пароля

1. Простые HTTP-запросы и ответы

Попытка 1: Гретти

Gretty — это Groovy-оболочка для Netty, асинхронного веб-сервера, написанная на Groovy ++. ( Вступительная статья для Гретти .)

Плюсы : Хорошо интегрирован с Groovy, прост в освоении, поддерживает обслуживание статических ресурсов и многое другое, Netty — это круто

Минусы : Недокументированный, проект кажется бездействующим, нет четкого способа добавить авторизацию пользователя и HTTPS.

Код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@GrabConfig(systemClassLoader=true)
@Grapes([
    @Grab('org.mbte.groovypp:gretty:0.4.279'),
    @Grab('mysql:mysql-connector-java:5.1.16')])
 
import org.mbte.gretty.httpserver.*
import groovy.sql.Sql
 
class Main {
 
    final def db = [url: 'jdbc:mysql://localhost:3306/user', user: 'dbUser', psw: 'dbPsw' ]
 
    def run() {
        startServer()
    }
 
    def getUser(def code) {
        println "Connecting to the DB to check '$code'..."
        def sql = Sql.newInstance( db.url, db.user, db.psw)
        return sql.firstRow("select * from users where code = $code") ?: "No such code found"
    }
 
    def startServer() {
        GrettyServer server = []
        server.groovy = [
                localAddress: new InetSocketAddress(6789), // no host => all
                defaultHandler: {
                    response.redirect "/"
                },
                "/:code": {
                    get {
                        def user = getUser(it.code)
                        response.text = "The code '${it.code}' refers to $user\n"
                        // => st. like: "The code 'abc' refers to [id:123, name:[email protected], code:abc]"
                    }
                }
        ]
        server.start()
        println "Groovy server is ready to serve"
    }
}
 
new Main().run()

пристань

Плюсы : зрелый, мощный, часто используемый во встроенной форме, поддерживает HTTPS и авторизацию (также программно) .

Ошибка : вы не можете использовать org.eclipse.jetty: jetty-server, потому что Grape.grab не сможет загрузить зависимость org.eclipse.jetty.orbit: javax.servlet из-за того, что Ivy запутался из- за упаковки и расширения. Используйте org.eclipse.jetty. вместо агрегата : jetty-server (пакеты агрегатов Jetty объединяют несколько меньших JAR-файлов).

Пример: Причал с безопасностью

(на основе статей о Embedded Jetty (включая SSL) для программной настройки и обработки запросов с помощью пользовательского обработчика или сервлета (действительно очень хорошо написано) и Как настроить безопасность с помощью Embedded Jetty для программной настройки аутентификации и авторизации)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import groovy.sql.Sql
import javax.servlet.*
import javax.servlet.http.*
import org.eclipse.jetty.server.*
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector
import org.eclipse.jetty.servlet.*
import org.eclipse.jetty.security.*
import org.eclipse.jetty.util.security.*
 
@GrabConfig(systemClassLoader = true)
@Grapes([
    @Grab('org.eclipse.jetty.aggregate:jetty-server:8.1.2.v20120308'),
    @Grab('org.eclipse.jetty.aggregate:jetty-servlet:8.1.2.v20120308'),
    @Grab(group='javax.servlet', module='javax.servlet-api', version='3.0.1'),
    @Grab('mysql:mysql-connector-java:5.1.16')])
class Main extends HttpServlet {
 
    final def db = [url: 'jdbc:mysql://localhost:3306/user', user: 'dbUser', psw: 'dbPsw' ]
 
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        final String code = request.pathInfo.substring(1); // skip leading '/'
        response.setContentType("text/plain");
 
        try {
            def user = getUser(code)
            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().println("Usage of the code '${code}': $user\n")
        } catch (Exception e) {
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
            response.getWriter().println("Connection to the database failed. This may be due to temporary " +
                    "connection problems or due to misconfiguration. Try later.")
        }
    }
 
    def getUser(def code) {
        println "Connecting to the DB to check '$code'..."
        def sql = Sql.newInstance( db.url, db.user, db.psw)
        return sql.firstRow("select * from users where code = $code") ?: "No such code found"
    }
 
    public static startServer() {
        Server server = new Server();
        server.setHandler(createServletHandlerWithAuthentication(
                "/", new Main(), createAuthenticationConstraint()))
        server.setConnectors((Connector[])[createSslConnector()])
        server.start();
        server.join();
    }
 
    /** Wrap the servlet in the servlet handler and configure it to run at the given URL, setting its security handler. */
    private static createServletHandlerWithAuthentication(String contextPath, Servlet servlet, SecurityHandler securityHandler) {
        final String pathSpec = "/*"
        ServletContextHandler servletHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS)
        servletHandler.setContextPath(contextPath)
        servletHandler.setSecurityHandler(securityHandler)
        servletHandler.addServlet(new ServletHolder(servlet), pathSpec)
        return servletHandler
    }
 
    /** Create HTTPS connector running at port 6789 and using key pair from the hard-coded keystore. */
    private static Connector createSslConnector() {
        SslSelectChannelConnector ssl_connector = new SslSelectChannelConnector()
        ssl_connector.setPort(6789)
 
        def cf = ssl_connector.getSslContextFactory()
        cf.setKeyStore(System.getProperty("user.home") + "/.keystore")
        cf.setKeyStorePassword("myKeystorePsw")
        cf.setKeyManagerPassword("myKeystorePsw")
 
        return ssl_connector
    }
 
    /** Create a security handler requiring authentication with username/password. */
    private static SecurityHandler createAuthenticationConstraint() {
        Constraint constraint = new Constraint();
        constraint.setName(Constraint.__BASIC_AUTH);
        constraint.setRoles((String[])["user"]);
        constraint.setAuthenticate(true);
 
        ConstraintMapping cm = new ConstraintMapping();
        cm.setConstraint(constraint);
        cm.setPathSpec("/*"); // auth. required for any URL
 
        def loginSrv = new HashLoginService()
        loginSrv.putUser("myLogin", new Password("myPassword"), (String[])["user"])
        loginSrv.setName("My App Realm")
 
        SecurityHandler sh = new ConstraintSecurityHandler()
        sh.setLoginService(loginSrv)
        sh.setConstraintMappings((ConstraintMapping[])[cm]);
 
        return sh
    }
}
 
Main.startServer()

Дополнительные ресурсы:

Winstone

Winstone — это сервлет-контейнер объемом 200 КБ, доступный через Maven , последний выпуск 2008 года. Похоже, он сосредоточен на обслуживании WAR.

Sun Java 6 HttpServer
Sun JRE 6+ содержит легкий, программно управляемый HTTP-сервер , поддерживающий также HTTPS. Пример кода .

2. Решения на основе REST

Джерси JAX-RS

Jersey, эталонная реализация JAX-RS (он же REST), может работать на встроенном тестовом сервере, таком как Grizzly, GlassFish или Jetty.

Плюсы : эталонная реализация JAX-RS, то есть стандартная.

Минусы : поиск и устранение неисправностей Джерси не так просто, как хотелось бы. Документация должна быть лучше (по сравнению с Jetty), это действительно слабое место (попробуйте найти что-нибудь о защите Джерси с помощью встроенного Grizzly).

Пример: Джерси со встроенным Гризли, без безопасности

(Если вы заинтересованы в безопасности и аутентификации, посмотрите пример проекта https-clientserver-grizzly . Мне кажется, он немного сложен.)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import groovy.sql.Sql
 
import javax.ws.rs.*
import javax.ws.rs.core.*
import com.sun.jersey.api.core.*
import com.sun.jersey.api.container.grizzly2.GrizzlyServerFactory
import org.glassfish.grizzly.http.server.HttpServer
 
@GrabConfig(systemClassLoader = true)
@Grapes([
    @Grab('com.sun.jersey:jersey-server:1.12'),
    @Grab('com.sun.jersey:jersey-core:1.12'),
    @Grab(group='com.sun.jersey', module='jersey-grizzly2', version='1.12'),
    @Grab(group='javax.ws.rs', module='jsr311-api', version='1.1.1'),
    @Grab('mysql:mysql-connector-java:5.1.16')])
 
@Path("/{code}")
class Main {
 
    final def db = [url: 'jdbc:mysql://localhost:3306/user', user: 'dbUser', psw: 'dbPsw' ]
 
    @GET @Produces("text/plain")
    public Response getUserByCode(@PathParam('code') String code) {
        try {
            def user = getUser(code)
            return Response.ok().entity("Usage of the code '${code}': $user\n".toString()).build();
        } catch (Exception e) {
            Response.serverError().entity("Connection to the database failed. This may be due to temporary " +
                    "connection problems or due to misconfiguration. Try later. Cause: $e".toString()).build();
        }
    }
 
    def getUser(def code) {
        println "Connecting to the DB to check '$code'..."
        def sql = Sql.newInstance( db.url, db.user, db.psw)
        return sql.firstRow("select * from users where code = $code") ?: "No such code found"
    }
 
    public static startServer() {
        ResourceConfig resources = new ClassNamesResourceConfig(Main)
        def uri = UriBuilder.fromUri("http://localhost/").port(6789).build();
        HttpServer httpServer = GrizzlyServerFactory.createHttpServer(uri, resources);
        println("Jersey app started with WADL available at ${uri}application.wadl")
        System.in.read();
        httpServer.stop();
    }
}
 
Main.startServer()

RESTEasy с помощью встроенного TJWS (веб-сервер Tiny Java и контейнер сервлетов)

TJWS полностью миниатюрный, занимает 100 КБ, работает также на Android, примерно в 5 раз меньше, чем конкуренты LWS и Jetty .

Из документации RESTEasy:

01
02
03
04
05
06
07
08
09
10
11
12
@Path("")
public class MyResource {
 
   @GET public String get() { return "hello world"; }
 
   public static void main(String[] args) throws Exception  {
      TJWSEmbeddedJaxrsServer tjws = new TJWSEmbeddedJaxrsServer();
      tjws.setPort(8081);
      tjws.getRegistry().addPerRequestResource(MyResource.class);
      tjws.start();
   }
}

Сам TJWS поддерживает SSL , я не уверен насчет плагина JBoss TJWS для RESTEasy (единственной версии tjws, доступной в Maven). Он также может быть встроен, но не доступен через Maven, и я не знаю, поддерживает ли он отображение запросов в код (вместо WAR и JSP).

Рестлет со встроенным сервером

См. Статью « Создание веб-приложений RESTful с использованием Groovy и Restlet», часть 1. Запуск и работа (2008 г.). Поскольку Restlet доступен в Maven, мы могли бы просто @Grab зависимости.

Еще более интересным является модуль GroovyRestlet, который позволяет программно настраивать авторизацию и обработку запросов , используя всего несколько строк. (Вы можете сделать это также на Java , добавив немного LoC.)

Документ для выпуска 2.1: Как реализовать авторизацию и HTTPS , самый простой из возможных REST-серверов в ~ 6 строках Java.

(Обратите внимание, что Restlet поставляется с простым HTTP-сервером, но также может использовать Jetty или Grizzly.)

Плюсы : RESt (хотя и нестандартный), хорошая интеграция с Groovy (хотя он может быть устаревшим)

Минусы : По состоянию на 4/2012 Restlet находится только в своем частном репозитории Maven, хотя они будут и в Maven Central , поддержка JAX-RS еще не полностью реализована (Restlet 2.1-RC3). Документация может быть лучше (более полные, более взаимосвязанные, более разнообразные примеры). Чтобы использовать HTTPS, вы должны выбрать другой сервер, а не внутренний.

Пример: Restlet + SimpleFramework Server + HTTPS и аутентификация (без интеграции Groovy)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import groovy.sql.Sql
import org.restlet.*
import org.restlet.data.*
import org.restlet.resource.*
import org.restlet.security.*
 
@GrabConfig(systemClassLoader = true)
@GrabResolver(name = 'restlet', root = 'http://maven.restlet.org')
@Grapes([
   @Grab('org.restlet.jse:org.restlet:2.1-RC3'),
   @Grab('org.restlet.jse:org.restlet.ext.simple:2.1-RC3'),
   @Grab('mysql:mysql-connector-java:5.1.16')])
class Main extends ServerResource {
 
    final def db = [url: 'jdbc:mysql://localhost:3306/user', user: 'dbUser', psw: 'dbPsw' ]
 
    @Get public String getUser() {
        def code = getRequestAttributes().get("code")
        def user = getUser(code)
        return "Usage of the code '${code}': $user\n"
    }
 
    def getUser(def code) {
 
        println "Connecting to the DB to check '$code'..."
        def sql = Sql.newInstance( db.url, db.user, db.psw)
        return sql.firstRow("select * from users where code = $code") ?: "No such code found"
    }
 
    public static startServer() {
        Component component = new Component();
        def userResourceFinder = component.getDefaultHost().createFinder(Main.class);
        component.getDefaultHost().attach("/{code}"
                , wrapResourceInAuthenticationCheck(component.getContext(), userResourceFinder));
        configureHttpsServer(component, 6789)
        component.start()
    }
 
    /**
     * Add a Guard (a filter) that asks the user for username/password and checks it against a map.
     */
    private static Restlet wrapResourceInAuthenticationCheck(Context context, Restlet resource) {
        MapVerifier verifier = new MapVerifier();
        verifier.getLocalSecrets().put("myLogin", "myPassword".toCharArray());
 
        ChallengeAuthenticator guard = new ChallengeAuthenticator(context.createChildContext(), ChallengeScheme.HTTP_BASIC, "My App");
        guard.setVerifier(verifier);
        guard.setNext(resource);
 
        return guard;
    }
 
    /**
     * Create the server, instruct it to use a SslContextFactory, and configure the factory with
     * our keystore and password. I guess that which server to use is determined by Restlet based on which
     * package (*.ext.simple.*, *.ext.jetty.* etc.) is available.
     */
    private static void configureHttpsServer(Component component, int port) {
        def secureServer = component.getServers().add(Protocol.HTTPS, port);
 
        // for params such as keystore path and password
        System.setProperty("javax.net.ssl.keyStorePassword", "myKeystorePsw") // used for keystorePassword & keyPassword
        def confg = secureServer.getContext().getParameters()
        confg.add("sslContextFactory", "org.restlet.ext.ssl.DefaultSslContextFactory")
        // Beware: keystorePath shall default to ${user.home}/.keystore but doesn't seem to do so => set it explicitly
        confg.add("keystorePath", "${System.getProperty('user.home')}/.keystore")
    }
}
 
Main.startServer()

Вывод

Возможно, я бы использовал Jetty, если REST не нужен, и Jersey + Jetty в противном случае (я бы определенно выбрал Jetty вместо Grizzly, так как документация намного лучше). Restlet также может быть интересным при условии, что интеграция Groovy работает, и если вы не возражаете против использования нестандартной реализации REST.

Глядя на длину примеров кода, было бы лучше попробовать Grails или st. в конце концов, похоже

Ссылка: Предоставление функциональности через HTTP с помощью Groovy и сверхлегких HTTP-серверов от нашего партнера JCG Якуба Холи в блоге Holy Java .