Мы находимся в процессе смены парадигмы, которая кардинально изменит то, как многие из нас создают и внедряют программное обеспечение. В конце 90-х программное обеспечение начало перемещаться в Интернет, и распространенность веб-браузера предоставила пользователям простой способ взаимодействия с программным обеспечением без необходимости устанавливать толстые клиентские приложения, которые было сложно обновить и, как правило, было недоступно для кроссплатформенности. Мы использовали браузер только как тупой терминал с очень ограниченными возможностями. «Логика приложения» происходила на сервере, и каждое взаимодействие требовало двусторонней передачи к серверу и обратно, который в результате должен был хранить тонны информации о состоянии пользовательского интерфейса. В некотором смысле эта модель лучше, чем подход толстого клиента, но в последующие годы мы обнаружили много недостатков этого подхода и сегодня,Сейчас мы возвращаемся к архитектуре клиент / сервер — и на этот раз современный веб-браузер «является» универсальным толстым клиентом.
В последние несколько лет веб-приложения часто используют браузер как нечто большее, чем просто тупой терминал с поддержкой HTML. В Ajax-революции многие из нас начали делать частичное обновление страниц и проверку формы на стороне клиента. Но чтобы идти дальше, нам нужны были более современные веб-браузеры с их более быстрыми механизмами исполнения JavaScript, более мощными возможностями CSS и большим количеством API. Сегодня это происходит под баннером «HTML5». В то время как HTML5 технически относится к набору стандартов браузера, для большинства разработчиков это действительно означает, что «браузер теперь универсальный мыслительный клиент». Рост числа мобильных приложений также способствует этому великому переходу к архитектуре клиент-сервер. В конечном счете, для нас с вами все это означает, что архитектура веб-приложений меняется.
Архитектура веб-клиента / сервера
Современная архитектура веб-приложений перемещает пользовательский интерфейс к клиенту, где все взаимодействия с пользователем обрабатываются на стороне клиента. Все состояние пользовательского интерфейса также перемещается на клиентскую сторону. Затем клиент просто делает вызовы на сервер, когда ему необходим доступ к общим данным или связь с другими клиентами / системами. Клиент общается с сервером через HTTP, используя шаблон RESTful или, возможно, более новый протокол WebSockets, который обеспечивает двунаправленную связь. Этот шаблон RESTful в основном представляет собой способ сопоставления стандартных HTTP-глаголов (таких как GET, POST, PUT, DELETE) с действиями, которые клиент хочет выполнить с внутренними / общими данными. Сегодня данные обычно сериализуются в JSON (нотация объектов JavaScript), потому что они просты и универсально поддерживаются. Службы RESTful также должны быть без гражданства,и никакие данные не должны храниться на веб-уровне между запросами (с возможным исключением кэшированных данных). В конечном итоге это превращает веб-уровень в простой прокси-слой, который обеспечивает конечную точку для сериализованных данных.
Существует много разных способов создания клиента на основе браузера, но для этого примера я буду использовать jQuery и JavaScript, поскольку они являются наиболее распространенными. Существует также много способов предоставления сервисов RESTful, здесь я буду использовать JAX-RS, поскольку это стандарт Java для сервисов RESTful. Для сохранения внутренних данных в этом примере также будет использоваться MongoDB, база данных NoSQL.
Код для этого примера доступен по адресу:
http://github.com/jamesward/jaxrsbars
Чтобы получить копию этого приложения локально, используйте git из командной строки или из вашей IDE и клонируйте следующий репозиторий:
git://github.com/jamesward/jaxrsbars.git
Сервер JAX-RS и MongoDB
Давайте начнем с настройки сервера. Как правило, это означает использование Apache Tomcat, создание файлов WAR и т. Д. В современной разработке приложений мы теперь видим, что разработчики Java тяготеют к подходу «без контейнеров», который значительно упрощает создание паритета dev / prod и очень хорошо работает для быстрых итераций разработки. Используя подход без контейнеров, обработчик HTTP обрабатывается как еще одна библиотека в вашем приложении. Вместо развертывания частичного приложения в контейнере, приложение содержит все, что ему нужно, и просто «запускается».
Вот файл pom.xml для сборки Maven, который включает в себя все зависимости, необходимые для настройки безконтейнерного сервера JAX-RS с использованием Jersey, Jackson и библиотеки обработки Grizzly HTTP:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.jamesward</groupId> <artifactId>jaxrsbars</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.glassfish.grizzly</groupId> <artifactId>grizzly-http-server</artifactId> <version>2.2.4</version> </dependency> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-grizzly2-http</artifactId> <version>2.0-m02</version> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json</artifactId> <version>2.0-m02</version> </dependency> <dependency> <groupId>net.vz.mongodb.jackson</groupId> <artifactId>mongo-jackson-mapper</artifactId> <version>1.4.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.4</version> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals><goal>copy-dependencies</goal></goals> </execution> </executions> </plugin> </plugins> </build> </project>
Обратите внимание, что этот проект не использует упаковку WAR. Вместо этого скомпилированные классы войдут в файл JAR. Это означает, что мне нужно предоставить классу Java хороший метод «статического пустого основного», который запускает сервер. Затем, предполагая, что путь к классам Java настроен на включение необходимых зависимостей, сервер можно запустить с помощью команды java. Плагин maven-dependency-plugin предоставляет простой способ скопировать зависимости проекта в один каталог, что упрощает установку пути к классам.
Вот класс com.jamesward.jaxrsbars.BarServer, который запускает сервер Grizzly, устанавливает соединение MongoDB и URL-адрес контента (о котором мы вскоре узнаем):
package com.jamesward.jaxrsbars; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import com.mongodb.DB; import com.mongodb.Mongo; import com.mongodb.MongoURI; import org.glassfish.grizzly.http.server.*; import org.glassfish.jersey.grizzly2.GrizzlyHttpServerFactory; import org.glassfish.jersey.media.json.JsonJacksonModule; import org.glassfish.jersey.server.Application; import org.glassfish.jersey.server.ResourceConfig; import javax.ws.rs.core.UriBuilder; public class BarServer { public static DB mongoDB; public static String contentUrl; private static final String CONTENT_PATH = "/content"; public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException { final int port = System.getenv("PORT") != null ? Integer.valueOf(System.getenv("PORT")) : 8080; final URI baseUri = UriBuilder.fromUri("http://0.0.0.0/").port(port).build(); final Application application = Application.builder(ResourceConfig.builder().packages(BarServer.class.getPackage().getName()).build()).build(); application.addModules(new JsonJacksonModule()); final HttpServer httpServer = GrizzlyHttpServerFactory.createHttpServer(baseUri, application); httpServer.getServerConfiguration().addHttpHandler(new StaticHttpHandler("src/main/webapp"), CONTENT_PATH); for (NetworkListener networkListener : httpServer.getListeners()) { if (System.getenv("FILE_CACHE_ENABLED") == null) { networkListener.getFileCache().setEnabled(false); } } Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { httpServer.stop(); } }); MongoURI mongolabUri = new MongoURI(System.getenv("MONGOLAB_URI") != null ? System.getenv("MONGOLAB_URI") : "mongodb://127.0.0.1:27017/hello"); Mongo m = new Mongo(mongolabUri); mongoDB = m.getDB(mongolabUri.getDatabase()); if ((mongolabUri.getUsername() != null) && (mongolabUri.getPassword() != null)) { mongoDB.authenticate(mongolabUri.getUsername(), mongolabUri.getPassword()); } contentUrl = System.getenv("CONTENT_URL") != null ? System.getenv("CONTENT_URL") : CONTENT_PATH; Thread.currentThread().join(); } }
Обратите внимание, что порт HTTP, URI подключения MongoDB и URL контента имеют значения по умолчанию для локальной разработки, но их также можно задать с помощью переменных среды. Это обеспечивает простой способ настройки приложения для различных сред. В конце этой статьи мы запустим это приложение на Heroku, где эти значения будут получены из среды. Существует также переменная среды с именем FILE_CACHE_ENABLED, которая позволяет включать и выключать статический кэш файлов. Отключение файлового кэша обеспечивает простой способ сократить цикл изменений / тестирования в процессе разработки, в то время как его исправление обеспечивает лучшую производительность в производстве.
Поскольку этот пример использует MongoDB для сохранения данных, если вы хотите выполнить локальную разработку для этого проекта, вам необходимо установить сервер MongoDB в вашей системе.
Чтобы запустить BarServer локально, сначала необходимо выполнить сборку Maven:
mvn package
Затем просто запустите BarServer, запустив:
java -cp target/classes:target/dependency/* com.jamesward.jaxrsbars.BarServer
Это приложение будет хранить список «баров», поэтому существует простой Java-объект с именем com.jamesward.jaxrsbars.Bar, содержащий:
package com.jamesward.jaxrsbars; import javax.persistence.Id; public class Bar { @Id public String id; public String name; }
Аннотация @Id будет использоваться библиотекой mongo-jackson-mapper для определения того, какое свойство будет хранить первичный ключ. У каждого бара также есть свойство name. В этом примере я использую простой прямой доступ к полю вместо более подробных методов получения и установки.
Для настройки конечных точек RESTful, которые будут предоставлять доступ для создания и выборки объектов Bar, существует объект Java с именем com.jamesward.jaxrsbars.BarResource, содержащий следующее. (Примечание: я опустил метод index, который вы увидите в источнике, но вскоре расскажу о нем.)
package com.jamesward.jaxrsbars; import net.vz.mongodb.jackson.JacksonDBCollection; import net.vz.mongodb.jackson.WriteResult; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import java.util.List; @Path("/") public class BarResource { private JacksonDBCollection<Bar, String> getJacksonDBCollection() { return JacksonDBCollection.wrap(BarServer.mongoDB.getCollection(Bar.class.getSimpleName().toLowerCase()), Bar.class, String.class); } @Path("bar") @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Bar addBar(Bar bar) { WriteResult<Bar, String> result = getJacksonDBCollection().insert(bar); return result.getSavedObject(); } @Path("bars") @GET @Produces(MediaType.APPLICATION_JSON) public List<Bar> listBars() { return getJacksonDBCollection().find().toArray(); } }
Метод getJacksonDBCollection использует подключение MongoDB, настроенное в BarServer, для предоставления доступа к коллекции MongoDB, в которой хранятся объекты Bar. У метода addBar есть Path of bar, что означает, что он будет доступен через HTTP-запросы к URL-адресу / bar. Он также обрабатывает только запросы POST, потребляет и производит контент application / json. Если вы запустите BarServer локально, вы можете протестировать этот метод с помощью команды curl:
curl -d '{"name":"foo"}' -H "Content-type:application/json" http://localhost:8080/bar
Данные JSON, отправляемые на сервер, автоматически десериализуются в экземпляр Bar. Этот объект добавляется в коллекцию MongoDB, и в результате возвращается полученный в результате Bar, содержащий первичный ключ. Вы должны увидеть что-то вроде:
{"id":"4f83185e986c2baad17bbf8b","name":"foo"}
Метод listBars просто выбирает все объекты Bar из коллекции MongoDB и возвращает объекты List of Bar. Для локального тестирования listBars с помощью curl:
curl -H "Content-type:application/json" http://localhost:8080/bars
Вы увидите массив JSON, содержащий список сериализованных объектов Bar, таких как:
[{"id":"4f83185e986c2baad17bbf8b","name":"foo"}]
Большой! Работает сервис RESTful для создания и извлечения объекта Bar. Теперь давайте взглянем на клиентскую часть приложения.
Клиент JavaScript и JQuery
Возможно, вы заметили, что BarServer устанавливает StaticHttpHandler, который ищет ресурсы в каталоге src / main / webapp и обрабатывает запросы к этим ресурсам через запросы к URL-адресу / content. Этот проект содержит копию минимизированной библиотеки jQuery JavaScript в файле src / main / webapp / jquery-1.7.min.js. Эта библиотека доступна через запросы к URL-адресу /content/jquery-1.7.min.js. Существует также файл src / main / webapp / index.js, который представляет собой весь клиентский / пользовательский интерфейс этого приложения. Он рендерит, создает и извлекает объекты Bar из сервисов RESTful, используя jQuery, и отображает HTML-форму и список объектов Bar. Вот содержимое файла index.js:
function loadbars() { $.ajax("/bars", { contentType: "application/json", success: function(data) { $("#bars").children().remove(); $.each(data, function(index, item) { $("#bars").append($("<li>").text(item.name)); }); } }); } function addbar() { $.ajax({ url: "/bar", type: 'post', dataType: 'json', contentType: 'application/json', data: JSON.stringify({name:$("#bar").val()}), success: loadbars }); } $(function() { $("body").append('<h4>Bars:</h4>'); $("body").append('<ul id="bars"></ul>'); loadbars(); $("body").append('<input id="bar"/>'); $("body").append('<button id="submit">GO!</button>'); $("#submit").click(addbar); $("#bar").keyup(function(key) { if (key.which == 13) { addbar(); } }); });
Функция loadbars делает ajax-запрос (используя jQuery) к / bars, а затем обновляет веб-страницу со списком. Функция addbar делает ajax-запрос к / bar, передавая ей строку JSON для объекта Bar, содержащего имя, указанное в поле ввода. Функция анонимной функции () {вызывается, когда страница готова. Эта функция добавляет неупорядоченный список, который будет содержать «бары» на страницу, вызывает функцию loadbars, добавляет элементы формы для создания новых баров и добавляет обработчики событий для нажатия на «GO!» кнопка / нажатие клавиши ввода в поле ввода. Оба этих обработчика событий вызывают функцию addbar.
Последнее, что нужно приложению, — это простая HTML-страница, которая загрузит приложение в браузере, загрузив библиотеку jQuery и JavaScript index.js. Это также может быть статический файл. Однако в скором времени мы загрузим клиентскую часть приложения (index.js и jquery-1.7.min.js) из сети доставки контента (CDN), что потребует изменения нашего загрузочного HTML-файла в зависимости от среды, в которой он работает. (таким образом, причина для свойства contentUrl в классе BarServer). Этот файл может быть обработан с использованием серверной библиотеки шаблонов, но в этом примере мы упростим задачу и просто добавим обработчик запросов GET в BarResource, который обрабатывает запросы к / URL и создает текстовое / html-содержимое:
@GET @Produces(MediaType.TEXT_HTML) public String index() { return "<!DOCTYPE html>\n" + "<html>\n" + "<head>\n" + "<script type='text/javascript' src='" + BarServer.contentUrl + "/jquery-1.7.min.js'></script>\n" + "<script type='text/javascript' src='" + BarServer.contentUrl + "/index.js'></script>\n" + "</head>\n" + "</html>"; }
Эта очень простая HTML-страница просто загружает jQuery и JavaScript-файл index.js, используя в качестве основы BarServer.contentUrl. Поскольку BarServer.contentUrl можно изменить с помощью переменной среды CONTENT_URL, теперь очень легко получить эти файлы из альтернативного местоположения. Однако они всегда доступны с этого сервера через URL-адрес / content.
Если вы запускаете приложение локально, вы сможете добавить новые «бары» и увидеть список, открыв http: // localhost: 8080 в вашем браузере. Если вы проверяете запросы, сделанные для отображения страницы (с помощью такого инструмента, как Firebug), вы должны увидеть четыре запроса:
Добавление нового «бара» из формы должно выполнить запрос POST, а затем обновить список «баров» с помощью запроса GET. Теперь, когда у нас есть полнофункциональное клиент-серверное веб-приложение, давайте развернем его в облаке и затем загрузим клиентскую часть из CDN.
Развертывание на облаке с Heroku
Для развертывания приложения в облаке вы можете использовать Heroku, Java-совместимый поставщик платформ Polyglot как услуга (PaaS). Для этого примера мы загрузим код через git в Heroku. Этот метод поддерживает «Непрерывную доставку», благодаря чему очень легко развертывать добавочные изменения. Каждый раз, когда Heroku получает новый код (посредством git push), запускается сборка Maven и развертывается новая версия. Выполните следующие действия, чтобы развернуть копию этого приложения на Heroku:
1) Регистрация для учетной записи Heroku . Вы сможете развернуть и запустить это приложение бесплатно на одном стенде .
2) Проверьте свою учетную запись Heroku . (Требуется использовать бесплатный уровень надстройки MongoLab.)
3) Установите инструментальный ремень Heroku .
4) Войдите в Heroku из командной строки:
heroku login
Это также поможет вам настроить ключ SSH и связать его с вашей учетной записью Heroku. Ключ SSH будет использоваться для аутентификации git-сообщений (загрузок).
5) Подготовьте новое приложение на Heroku (используя стек кедров ) с помощью дополнения MongoLab (выполните эту команду в корне вашего проекта):
heroku create --stack cedar --addons mongolab
Удаленная конечная точка git для вновь созданного приложения будет называться «heroku» и будет автоматически добавлена в вашу конфигурацию git.
6) Используя git, загрузите ваше приложение в Heroku:
git push heroku master
Вы также можете сделать это в вашей IDE. Это скопирует исходные файлы, дескриптор сборки Maven и Procfile в Heroku. Затем Heroku запустит сборку Maven для проекта, развернет приложение на dyno и запустит веб-процесс, определенный в Procfile. Procfile — это простой способ привязать имена процессов к командам. Вот Procfile для этого проекта:
web: java -cp target/classes:target/dependency/* com.jamesward.jaxrsbars.BarServer
Обратите внимание, что команда запуска в Heroku такая же, как и для запуска BarServer локально.
После завершения git push вы сможете открыть свое приложение в браузере, используя либо URL из вывода git push, либо запустив:
heroku open
7) По умолчанию кэширование и обработка 304 были отключены для локальной разработки. Включите это, установив переменную среды FILE_CACHE_ENABLED в Heroku:
heroku config:add FILE_CACHE_ENABLED=true
Вы можете увидеть полный список переменных среды для вашего приложения (включая MONGOLAB_URI, который был установлен дополнением MongoLab), выполнив:
heroku config
Congrats! Ваше приложение теперь работает в облаке!
Обслуживание клиентской части из AWS CloudFront CDN
В этот момент все должно чудесно работать на облаке. Но мы можем сделать еще один шаг вперед, чтобы повысить производительность приложения. CDN будут по преимуществу фиксировать статические активы по всему миру, а это означает, что копия контента размещается очень близко к потребителю контента. Этот процесс действительно работает только для статических ресурсов, но поскольку клиентская часть этого приложения теперь является статическими активами (index.js и jquery-1.7.min.js), эти файлы можно загружать из CDN, а не с более централизованного сервера на Heroku. HTML-страница, которая загружает приложение, будет по-прежнему загружаться непосредственно из Heroku, потому что она не полностью статична, и потому что мы хотим избежать ограничений браузера из разных источников. Во избежание ограничений браузера из разных источников страница, которую пользователь загружает в свой браузер, должна находиться в том же домене, что и службы RESTful.
For this example we will use the Amazon CloudFront CDN service. Amazon uses a purely usage based pricing model so this part won’t be free, but for this small example app it shouldn’t cost more than a dollar to try it out. CloudFront provides a way to retreive the static assets that it will cache, from an origin server. This makes it very easy to configure and switch to using CloudFront for static assets. If CloudFront does not have the correct version of the static asset it will go back to the origin server to get it.
Follow these steps to setup CloudFront:
1) Create an account on AWS:
https://aws-portal.amazon.com/gp/aws/developer/registration/index.html
3) Open: https://console.aws.amazon.com/cloudfront/home
4) Click «Create Distribution»
5) Select «Custom Origin»
6) Enter the domain name of your application on Heroku
7) Select «Continue»
8) Select «Continue» again
9) Select «Create Distribution»
10) Wait a few minutes while the CloudFront Distribution is provisioned
11) Using the newly created CloudFront distribution, configure your Heroku app to use CloudFront for static content:
heroku config:add CONTENT_URL=http://yourdomainname.cloudfront.net/content
12) Reload your app in your browser
You now have a Client/Server web app in the cloud with the client-side edge cached on a CDN! You can now expand on this example to build out much more complex applications that have a distinct Client/Server separation. As you explore this new way of building applications you will begin to discover a vast and emerging variety of tools and libraries to help you build the client-side. I recommend that you check out Backbone.js for client-side MVC and Underscore.js for client-side templating. For beautifying the UI check out Twitter Bootstrap. There are many different choices that you should evaluate but those are good libraries to start with.
If you’d like to see a live demo of this project check out: http://jaxrsbars.herokuapp.com
END