REST-сервис на Java — это просто

    Многим программистам Java-технологии могут показаться монструозными и сложными для понимания. В этой небольшой статье я бы хотел показать, что при желании можно собрать приложение из довольно простых компонентов, не прибегая к мега-фреймворкам.



    В качестве примера я выбрал простенький REST-сервис. Для описания ресурсов будет использоваться Jersey. Как бонус, будет показано использование Dependency Injection фреймворка Google Guice. Можно было бы и без него, но я не хочу, что бы пример показался слишком игрушечным и оторванным от жизни. Весь пример я постараюсь уложить в примерно 50 строк в одном файле и не будет использовано ни строчки XML.

    Итак, поехали:

    1. Опишем простенький класс, который будет предоставлять доступ к некоторой информации. Пусть это будет счетчик вызовов:
     @Singleton
     public static class Counter {
      private final AtomicInteger counter = new AtomicInteger(0);
      public int getNext() {
       return counter.incrementAndGet();
      }
     }


    * This source code was highlighted with Source Code Highlighter.


    Аннотация Singleton нужна для указания джуйсу, что объект должен быть синглтоном :)

    2. Опишем сервис, который будет возращать нам что-то, попутно дергая counter:
     @Path("/hello")
     public static class Resource {

      @Inject Counter counter;

      @GET
      public String get() {
       return "Hello, User number " + counter.getNext();
      }
     }


    * This source code was highlighted with Source Code Highlighter.


    3. Теперь подружим Jersey и Guice. Я воспользовался готовой интеграцией, она называется jersey-guice. Интеграция осуществляется через сервлет/фильтр GuiceContainer, для использования которого нужно объявить ServletModule из расширения guice-servlet-module и указать, что нужные нам запросы будут обрабатываться GuiceContainer, что позволит объявлять Jersey ресурсы в контексте Guice.

     public static class Config extends GuiceServletContextListener {
      @Override
      protected Injector getInjector() {
       return Guice.createInjector(new ServletModule(){
        @Override
        protected void configureServlets() {
         bind(Resource.class);
         bind(Counter.class);
         serve("*").with(GuiceContainer.class);
        }
       });
      }
     }


    * This source code was highlighted with Source Code Highlighter.


    Там же мы забайндили Counter и Resource.

    4. Осталось запустить все это добро, используя сервлет-контейнер. Для этого нам совершенно не обязательно собирать war-ку и деплоить в какой-нибудь Tomcat. Можно воспользоваться встраиваемым контейнером. На ум приходят Jetty и Grizzly. Я выбрал последний. Вот код, который запускает сервер:

     public static void main(String[] args) throws Exception {
      int port = Integer.valueOf(System.getProperty("port"));
      GrizzlyWebServer server = new GrizzlyWebServer(port);
      ServletAdapter adapter = new ServletAdapter(new DummySevlet());
      adapter.addServletListener(Config.class.getName());
      adapter.addFilter(new GuiceFilter(), "GuiceFilter", null);
      server.addGrizzlyAdapter(adapter, new String[]{ "/" });
      server.start();
     }


    * This source code was highlighted with Source Code Highlighter.


    Обратите внимание, что пришлось объявить пустой сервлет:

     @SuppressWarnings("serial")
     public static class DummySevlet extends HttpServlet { }


    * This source code was highlighted with Source Code Highlighter.


    Он нужен, что бы Guice-фильтр сработал. Если не будет ни одного сервлета, Grizzly не будет передавать запрос никаким фильтрам.

    Вот пожалуй и все. Далее приведу весь код:

    public class App {
      
      @Path("/hello")
      public static class Resource {
        
        @Inject Counter counter;
          
        @GET
        public String get() {
          return "Hello, User number " + counter.getNext();
        }
      }
      
      @Singleton
      public static class Counter {
        private final AtomicInteger counter = new AtomicInteger(0);
        public int getNext() {
          return counter.incrementAndGet();
        }
      }

      public static class Config extends GuiceServletContextListener {
        @Override
        protected Injector getInjector() {
          return Guice.createInjector(new ServletModule(){
            @Override
            protected void configureServlets() {
              bind(Resource.class);
              bind(Counter.class);
              serve("*").with(GuiceContainer.class);
            }    
          });
        }    
      }
      
      @SuppressWarnings("serial")
      public static class DummySevlet extends HttpServlet { }
      
      public static void main(String[] args) throws Exception {
        int port = Integer.valueOf(System.getProperty("port"));
        GrizzlyWebServer server = new GrizzlyWebServer(port);
        ServletAdapter adapter = new ServletAdapter(new DummySevlet());
        adapter.addServletListener(Config.class.getName());
        adapter.addFilter(new GuiceFilter(), "GuiceFilter", null);
        server.addGrizzlyAdapter(adapter, new String[]{ "/" });
        server.start();
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    UPD: Исходники
    Share post

    Comments 34

      +2
      А чем плохо использовать нативные средства java ee для имплементации сервисов?
        0
        Не плохо, просто можно делать проще. KISS, как говорится :) Мой пример не просто игрушка, у меня есть опыт работы с реально работающими приложениями, написаными похожим образом. Подход хорошо зарекомендовал себя, а именно: легко писать тесты, легко деплоить, т.к. не нужен внешний контейнер, jar-ник можно легко покавать в deb, минималистичные библиотеки вроде guice легко поддаются расширению, очень простое конфигурирование приложения.
          +1
          ну, если Jersey да еще Grizzle, то по KISS логично остановиться на Glassfish Web Profile, а guice — лишняя сущность :)
            0
            Зависит от целей, которые вы преследуете. У меня в приоритете простой, unix-way деплой.
              +1
              а сложно паковать Glassfish в deb? В любом случае есть Embedded Glassfish.
                +1
                Боюсь сейчас все скатится к холивару javaee vs альтернативы. Давайте остановимся на том, что я не использую app-серверы :) Просто не нравится такой подход в целом.
                  0
                  холивар, конечно, зло, но я как раз не против альтернатив, а за то, чтобы строго соблюдать неприкосновенность своей лени и пользоваться готовыми решениями. Ведь всегда хорошо, когда за тебя умные дяди уже подумали.
                    0
                    Хм. В общем случае умные дяди не могут подумать «за тебя», только лишь за некого усредненного разработчика.

                    И под простотой мы видимо разные вещи понимаем. Я склоняюсь к тому, что более простые решения как раз требуют большего интеллектуального вклада. «Ленивые» решения — это как правило лишь локальный оптимум.
                      0
                      В данном случае дяди подумали именно за меня, используя Glassfish я просто ставлю те же аннотации @Singleton, @Path и @Get и все работает. REST в Java это проще :)
                        0
                        Дело в том, что я не стремлюсь, что бы за меня кто-то думал. Это для меня не есть критерий простоты.
                          0
                          Ну вот ставьте и успокойтесь уже, и не склоняйте людей на свою сторону зла.
                          Мы вас не трогаем — вы нас не трогаете.
                          Не навязывайте нормальным людям свои страшные монструозные java ee-шные «фреймворки».
                0
                Совершенно с вами согласен. Из-за этого пример ещё более синтетически выглядит чем мог быть. Глаз за класс Counter цепляется сразу и хочется его банально удалить :)
                  0
                  А я совершенно с ним несогласен.
                  Это камент для статистики.
            0
            Неплохой пост.
            Есть еще Restlet framework аналог Jersey.
            Правда сейчас в своем проекте использую сервлеты с GSON сериализатором для вывода в json.
              0
              А можно было не создавать пустой сервлет, а передать DefaultServelet в Guice?
              +1
              Может, стоит в таких статьях maven скрипт публиковать или ссылку на тарбол, чтобы можно было скачать и поиграться 5 минут.
                0
                Оно у меня на другом компе осталось, как доберусь выложу, конечно :)
                  0
                  Тарбол… какое древнее слово, а
                    0
                    Добавил ссылку на исходники в svn
                      0
                      мавен не нужен
                      +1
                      Рекомендую взглянуть на Spring MVC [1, 2] или CXF.
                        0
                        Первым пользуюсь. Все же связка spring + jersey для rest по-удобнее spring mvc, а связка guice + jersey мне понравилась еще больше :) Второй — просто еще одна имплементация JAX-RS (и не только) на ряду с Jersey, вполтную не пользовался, так что не знаю дружит ли он с guice.
                          0
                          Все же связка spring + jersey для rest по-удобнее spring mvc

                          Почему? Сейчас стою как раз перед выбором?
                            0
                            Вся-таки Jersey (JAX-RS) специально создавался для создания REST-сервисов. Чуть API более лаконичен именно для REST-сервисов. Хотя разница в целом мизерная. Если выберете spring mvc 3, то ничего особо не потеряете.
                        0
                        Недавно обсуждали, может быть кому-то покажется интересным
                        community.livejournal.com/ru_java/1019047.html
                          0
                          Извините за занудство но тут «И так, поехали:», «итак» слитно, по-моему, надо писать в данном случае, просто глаз резануло, в любом случае спасибо за статью, очень интересно.

                          возник вопрос, может немного не в тему. видно, что вы применяете guice активно. мы на работе много дела имеем с подобного рода сервисами, обычно используется связка spring+jetty. поскольку банк большой, процессы сложные, надо деплоить приложение то туда, то сюда, на разные энвайроменты, соответственно проблема решается стандартным образом — в спринг конфигурации используются плейсхолдеры на проперти, есть файлы пропертей для каждого энвайромента, типа dev, UAT, SIT, Prod. ну а проперти включают себя параметры подключения к базе, к jms и к другим системам

                          при запуске приложение получает параметер типа -Denv=dev и файл с пропертями всасывается в спринг конфигурацию.

                          в свое время любопытствовал по поводу guice, но так и не воткнул, как в нем решать проблему множественности конфигураций. вы берете порт из системных параметров, неужели приходится все параметры пачкой передавать через -Dxxx=yyy? или используете адские скрипты, которые поставляют проперти в системные переменные? или вообще guice для такого рода приложений не годится?

                            0
                            «Итак» поправил, спасибо.

                            Насчет guice. Мы конфигурируем там: через -D передается путь к property файлу, который потом байндится как guice-бин. При создании guice-контекста этот файлик само собой используется для конфигурировании бинов. jar-ка вместе c property файлом пакуется в deb. После первой установки пакета в конекретном окружении property файл правится. При переустановке пакета проверка изменений в property файле осуществляется dpkg автоматически, если изменений «пакетного» конфига нету, то правленый конфиг оставляется нетронутым.

                            Ту схему, что описали вы, я думаю можно запросто повторить и в guice. Просто вместо place holders будет извлечение проперти из конфига в java-коде.

                              0
                              спасибо за разьяснение, идею понял, попробую сделать так в каком-нибудь проекте своем. Еще jar-ка очень улыбнула :) как вы с ними нежно, мы их все — жарник, или жар
                            +1
                            Спасибо, интересно. Теперь у меня на один повод меньше не любить java.
                              0
                              Насколько я понял, в описанном примере поднимается свой джуйс. А что делать в случае, когда Jersey ресурс подключается к большому проекту со своим джуйсом и нужно брать классы именно из него?
                                +1
                                Когда мы передаем в метод Guice.createInjector наши модули, то все они будут работать в одном контексте. Бины из одного модуля тогда смогут инжектить бины из другого.

                                ЗЫ. Случайно не свои люди? :)
                                  0
                                  Да, свои. Мир дейстительно тесен. (:

                              Only users with full accounts can post comments. Log in, please.