Статьи

В банку ада и обратно

В этом сообщении мы используем  JHades  для устранения проблем в нашем пути к классам. JHades — это мощный инструмент, который дает нам полезную информацию при попытке решить  определенный набор проблем  в разработке Java.

Демонстрация этого поста основана на следующем:  генерация кода RAML .

Это не имеет ничего общего с RAML как таковым. Я просто буду использовать демонстрацию,  представленную в этом посте, и продолжу ее  (обратите внимание, что «результирующее» состояние проекта каждого поста находится в отдельной ветке).

Итак, прежде чем мы сможем добраться до сути вещей, нам нужно решить проблему.

Идите вперед и попробуйте git clone https://github.com/ricston-git/blog-contacts-app && cd blog-contacts-app && git checkout генерация raml-кода (это демонстрация позади предыдущего поста). Теперь перейдите в контакт-приложение и установите mvn. Перейдите в contacts-app-impl и mvn exec: java -Dexec.mainClass = «com.ricston.contacts.Main». Если ваша переменная JAVA_HOME env указывает на установку Java 6, вы получите: Unsupported major.minor version 51.0. Это потому, что я обновил Jersey до 2.13 — поэтому вы захотите указать его в домашнем каталоге Java 7. Затем вы должны увидеть, что сервер прослушивает порт 4433 — это означает, что наш сервер работает, и мы можем отправлять ему запросы, как определено в нашем файле RAML.

Так что … без проблем — приложение работает отлично. Это потому, что вы используете фиксированную версию :)

Для этого вам нужно открыть файл pom.xml contacts-app-api и удалить область, указанную в зависимости от raml-jaxrs-codegen-core. Это (и несколько других ненужных вещей) было состоянием проекта, когда я начал работать над демо-кодом за постом в блоге, который я собирался написать изначально (затем я закончил писать этот). На самом деле произошло то, что я смог продолжить работу, не сталкиваясь с этой проблемой в течение достаточно долгого времени, потому что я запускал программу из своей среды IDE. Я установил еще один модуль Maven с военным пакетом, перенес конфигурацию из Main.java в дескриптор развертывания web.xml и запустил все без проблем в моей IDE. Только когда я попытался запустить проект из Maven с помощью плагина Tomcat — mvn clean tomcat7: run, я столкнулся со следующим исключением.Это исключение будет тем же, которое вы получите в этот момент, если вы установите все заново и запустите mvn, как только что сделали (только на этот раз у вас должна быть область компиляции по умолчанию для зависимости raml-jaxrs-codegen-core):

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:297)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.NoSuchMethodError: javax.ws.rs.core.Application.getProperties()Ljava/util/Map;
	at org.glassfish.jersey.server.ApplicationHandler.<init>(ApplicationHandler.java:304)
	at org.glassfish.jersey.server.ApplicationHandler.<init>(ApplicationHandler.java:285)
	at org.glassfish.jersey.servlet.WebComponent.<init>(WebComponent.java:311)
	at org.glassfish.jersey.servlet.ServletContainer.init(ServletContainer.java:170)
	at org.glassfish.jersey.servlet.ServletContainer.init(ServletContainer.java:358)
	at javax.servlet.GenericServlet.init(GenericServlet.java:244)
	at org.eclipse.jetty.servlet.ServletHolder.initServlet(ServletHolder.java:532)
	at org.eclipse.jetty.servlet.ServletHolder.doStart(ServletHolder.java:344)
	at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:64)
	at org.eclipse.jetty.servlet.ServletHandler.initialize(ServletHandler.java:791)
	at org.eclipse.jetty.servlet.ServletContextHandler.startContext(ServletContextHandler.java:265)
	at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:717)
	at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:64)
	at org.eclipse.jetty.server.handler.HandlerWrapper.doStart(HandlerWrapper.java:95)
	at org.eclipse.jetty.server.Server.doStart(Server.java:282)
	at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:64)
	at com.ricston.contacts.Main.main(Main.java:35)
	... 6 more
Так что же нам говорит эта трассировка стека? Очевидно, javax.ws.rs.core.Application загружается из некоторого jar-файла, но у него нет метода getProperties (), который возвращает java.util.Map.

Когда вы получаете такое поведение; программа работает нормально, когда выполняется из вашей IDE (например), но выдает исключение при выполнении в другой среде (в данном случае от Maven), у вас есть веские основания полагать, что у вас есть проблема с Jar-адом. Под «такого рода исключениями» я подразумеваю исключение, которое намекает на то, что в вашем classpath может быть несколько jar-файлов, имеющих одинаковое имя класса в одном и том же пакете, но с другой реализацией. Исключения при использовании класса и отсутствие ошибок такого метода — обычные признаки ада Джара. Опять же, сами по себе это не так, но когда вы получаете такие вещи при работе в определенной среде, но не в другой, то это довольно хороший признак того, что у вас возникают проблемы с адом в Jar.

Хорошо, теперь у нас проблемы. Очевидно, мы не можем просто полагаться на случайность, надеясь, что правильный класс загружен в среде, в которой мы развертываем. Технически, если бы у меня был пакет mvn в модуле Maven, упомянутый выше, я бы получил свой файл war и развернул его в своей локальной установке Tomcat, в которой — как это происходит — загружается нужный класс, а это исключение не ‘ поднимите свою уродливую голову. Но это не решает проблему.

Здесь нам действительно нужно больше информации. Было бы замечательно, если бы NoSuchMethodError также давал нам местоположение jar-файла, из которого загружался класс javax.ws.rs.core.Application при сбое. Мы не получаем этого, но мы можем использовать инструмент под названием  JHades,  чтобы сообщить нам, если у нас есть повторяющиеся классы в банках в нашем classpath. Эта информация практически решит эту проблему.

Идите дальше и добавьте jhades (не jhades-standalone-report) в pom contacts-app-impl — используйте последнюю версию  здесь :

<dependency>
    <groupId>org.jhades</groupId>
    <artifactId>jhades</artifactId>
    <version>1.0.4</version>
</dependency>
Теперь поместите следующее в метод main.java contacts-app-impl (в начале):

new JHades().overlappingJarsReport();

При установке и запуске, как и раньше, вы должны получить список перекрывающихся jar-файлов в выводе консоли вместе с предыдущим сообщением об ошибке. Важной частью является следующее:

file:/Users/justin/.m2/repository/javax/ws/rs/jsr311-api/1.1.1/jsr311-api-1.1.1.jar overlaps with
file:/Users/justin/.m2/repository/javax/ws/rs/javax.ws.rs-api/2.0/javax.ws.rs-api-2.0.jar - total overlapping classes: 55 - same classloader ! This is an ERROR!

Из сообщения об ошибке мы знаем, что проблемным классом является javax.ws.rs.core.Application, поэтому имеет смысл сосредоточиться на столкновении с участием jsr311-api-1.1.1.jar и javax.ws.rs.api-2.0. баночка. Конечно же, если мы откроем класс javax.ws.rs.core.Application в обоих этих jar-файлах, мы увидим, что класс jsr311-api-1.1.1.jar  не  имеет метода getProperties (), который наш Код пытается использовать. Тот, который сделал injavax.ws.rs.api-2.0.jar. Обратите внимание: если у вас нет источников в вашем локальном репо, вы сможете получить их, запустив mvn dependency: sources в каталоге родительского модуля contacts-app.

Наконец, мы почти в корне проблемы здесь. Последняя часть головоломки — выяснить, что вытягивает jsr311-api-1.1.1.jar и удаляет его. Так как мы фактически начали с решения, вы уже знаете, что зависимость contacts-app-api от raml-jaxrs-codegen-core вызывает jsr311-api-1.1.1.jar — но, если вы этого не сделали, как бы вы поступили об этом?

То, что я сделал, просто вывел вывод mvn dependency: tree во временный файл: mvn dependency: tree >> dependency-tree и поищем jsr311 (или, может быть, grep для него с некоторым контекстом, например mvn dependency: tree | grep -C 20 jsr311, если вы не хотите создавать файл). В любом случае, мы получаем необходимую нам информацию:

[INFO] +- com.ricston:contacts-app-api:jar:1.0.0-SNAPSHOT:compile
[INFO] |  \- org.raml:raml-jaxrs-codegen-core:jar:1.0.OA-SNAPSHOT:compile
[INFO] |     +- org.raml:raml-parser:jar:0.8.7:compile
[INFO] |     |  +- org.yaml:snakeyaml:jar:1.13:compile
[INFO] |     |  +- commons-validator:commons-validator:jar:1.3.1:compile
[INFO] |     |  |  +- commons-beanutils:commons-beanutils:jar:1.7.0:compile
[INFO] |     |  |  +- commons-digester:commons-digester:jar:1.6:compile
[INFO] |     |  |  |  +- commons-collections:commons-collections:jar:2.1:compile
[INFO] |     |  |  |  \- xml-apis:xml-apis:jar:1.0.b2:compile
[INFO] |     |  |  \- commons-logging:commons-logging:jar:1.0.4:compile
[INFO] |     |  \- org.kitchen-eel:json-schema-validator:jar:1.2.2:compile
[INFO] |     |     \- org.mozilla:rhino:jar:1.7R4:compile
[INFO] |     +- javax.ws.rs:jsr311-api:jar:1.1.1:compile
Генератор кода извлекает банку, которую мы хотим, и, если подумать, на самом деле нет смысла включать raml-jaxrs-codegen-core в качестве зависимости, которая переносится, когда зависит от contacts-app-api в другом проекте. Генератор кода используется только для генерации кода — то есть для создания наших contacts-app-api. Это не то, что нам нужно во время выполнения. Следовательно, использование области действия, представленной в генераторе кода, приведет к тому, что мы добьемся того, чтобы включить зависимость при сборке contacts-app-api, но не в зависимости от этого из другого модуля.

Теперь я хотел бы быстро продемонстрировать, как использовать JHades в других сценариях. Давайте повторим проблему, которую мы имеем здесь, в контексте модуля War Maven, где у нас нет такой роскоши, как просто добавить некоторый код для распечатки перекрывающихся jar-файлов.

Итак, давайте продолжим и добавим еще один модуль Maven — «contacts-app-server». Теперь структура нашего проекта выглядит примерно так:

── contacts-app
│  └── pom.xml
│  ├── contacts-app-server
│     ├── pom.xml
│     ├── src
├── contacts-app-api
│  ├── pom.xml
│  └── src
└── contacts-app-impl
   └── src
Мы добавим mkdir -p src / main / webapp / WEB-INF в контакт-сервер приложений и добавим web.xml в WEB-INF:

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>contacts-app-server</display-name>
<!--     REST API servlet: -->
    <servlet>
        <servlet-name>restService</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>com.ricston.contacts.app.ContactsApp</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>restService</servlet-name>
        <url-pattern>/api/*</url-pattern>
    </servlet-mapping>
</web-app>
Здесь нет ничего особенного. Мы практически перешли от запуска приложения из Main.java к возможности упаковать его как войну и запустить его в контейнере сервлетов, таком как Tomcat. (Обратите внимание, что вы можете получить pom.xml, который здесь не показан для краткости, из демо-репозитория под веткой с тем же именем, что и в этом сообщении в блоге — просто помните, что генератор кода имеет область действия, предоставленную там).

Если вы попробуете запустить contacts-app-server с помощью mvn clean tomcat7: run, вы получите эту ошибку:

java.lang.ClassCastException: org.glassfish.jersey.servlet.ServletContainer cannot be cast to javax.servlet.Servlet

На самом деле это ошибка, которая не имеет ничего общего с тем, что нас интересует. Я не совсем уверен, почему, но причины Jetty, которые мы имеем в contacts-app-impl, вызывают это. Я могу открыть ServletContainer в своей среде IDE и увидеть, что расширяемый HttpServlet извлекается из банки с координатами Maven: org.eclipse.jetty.orbit: javax.servlet: 3.0.0.v201112011016 — (выглядит как один сумасшедший номер версии).

В любом случае, мы не будем использовать Main.java для размещения нашего веб-приложения, поэтому удалите его вместе с зависимостями Jetty в pom.xml contacts-app-impl. Тогда вы сможете снова запустить tomcat из maven и получить NoSuchMethodError.

Чтобы получить необходимую нам информацию, у нас есть два варианта (по крайней мере, я знаю 2 варианта). Мы можем либо опустить зависимость от JHades, как мы делали раньше, и добавить следующее в наш web.xml (в начале):

<listener>
        <listener-class> org.jhades.JHadesServletListener</listener-class>
</listener>
Это должно снова дать нам необходимую информацию (добавить вывод во временный файл).

Кроме того, вы можете получить jardes-standalone-report jar (либо  загрузите  его вручную, либо полагайтесь на него, и позвольте Maven установить его в вашем локальном репо). Это позволяет нам запустить следующую команду на выходе нашего проекта (файл war):

java -jar /Users/justin/.m2/repository/org/jhades/jhades-standalone-report/1.0.4/jhades-standalone-report-1.0.4.jar ./target/contacts-app-server-1.0. 0-SNAPSHOT.war

Примечание: при использовании mvn tomcat7: run, похоже, удаляет файл war из целевого каталога, поэтому вам придется снова пакет mvn.

В любом случае вы сможете прийти к тому же выводу, что и раньше, с выводом JHades.

В конце концов я перейду к написанию поста в блоге, который я изначально имел в виду (этот вид был просто разработан при попытке чего-то другого). Настройтесь снова в следующий раз для еще одного модуля, который мы добавим к этому проекту. :)

В следующий раз мы будем настраивать интерфейсный модуль с помощью  Yeoman  (используя магистральный генератор) — создадим наш проект с Grunt из Maven и будем зависеть от нашего блестящего нового интерфейсного модуля с нашего сервера контактов-app-server.

До скорого :)