Java-based конфигурирование embedded Jetty/Spring MVC/Spring Security

Кому-то нравится исключительно xml-конфигурация, так как позволяет собрать всю настройку проекта если не в одном файле, то в одной папке с конфигурационными файлами точно, кому-то нет. К примеру, я считаю, что времена persistence.xml ушли безвозвратно — отображения сущностей для ДБ удобнее прописывать в классах сущностей, а маппинги контроллеров и методов удобнее назначать непосредственно в классах контроллеров. Вывод очевиден: если разработчик не страдает анахронизмом, то так или иначе проект будет иметь смешанную настройку — часть в аннотациях, часть в xml-файлах. Это не хорошо и не плохо, это данность времени — все привыкли к web.xml, application.xml, springmvc-servlet.xml и т.д., но при этом активно пользуются аннотациями @ Controller, @ Repository, @ Autowired, @ Value, @ ResponseBody и т.д. Начиная с версии 3 Spring добавил возможность использования Java-based конфигурации, что позволило полностью отказаться от xml-файлов в настройке проекта. Ранее на Хабре была публикация с анализом преимуществ и недостатков одних способов перед другими, с рядом плюсов xml-конфигурации я полностью согласен.

Однако в данной публикации речь не о преимуществах одних или недостатков других, и тем более не попытка агитации за новые технологии и возможности, это только лишь консолидированный отчет о сборке веб-проекта с нуля до рабочего проекта. И главное — с включением в проект embedded сервера Jetty. На самом деле отдельных фрагментов по настройке как встроенного Jetty, так и контекстов Spring Application, MVC, Security, навалом. Но чтобы разобраться в тонкостях этих настроек, не внедрять сложную структуру и не городить костыли, потребовалось кое-какое время и просмотр большого числа документации и исходников. Но даже имея значительный опыт настройки, пришлось столкнуться с рядом затруднений, после решения которых пришла идея опубликовать все это в одном месте.

Раньше мне больше нравилась архитектура проекта, которая базировалась на standalone веб-сервере (обычно Tomcat), в настройках которого прописывались контексты веб-проектов, которые определялись вместе со стартом веб-сервера. Такая архитектура удобна для небольших проектов, которые удобно запускать пачкой на одном веб-сервере, не создавая под каждый проект свой инстанс или другой веб-сервер. Но со временем проекты становятся крупнее, серьезнее, и такой подход перестает удовлетворять их запросам. Решение со встроенным веб-сервером часто себя оправдывает. Также это удобно для разработки: запуск в IDE прост, не требует внешних программных зависимостей, развертывание проекта и его старт также не вызывают трудностей и делаются «в одно касание». Таким образом, вопрос встал не в том, чтобы решить какую-то задачу, а просто «задачка ради задачки»: избавиться от всех xml, иметь «на борту» embedded Jetty и запустить веб-проект на Spring MVC с использованием Spring Security.

Забегая вперед, сразу скажу, что не имея в проекте ни одного xml-файла, я не избавился от всех xml-конфигурационных файлов — звучит провокационно и бессмысленно, но это так: embedded Jetty в пакете org.eclipse.jetty.webapp содержит файл webdefault.xml, содержащий дефолтный дескриптор развертывания веб-приложения. От него можно отказаться, прописывая дефолтные сервлеты DefaultServlet и JspServlet, дефолтные листенеры и энкодеры — около 500 строк xml-кода. Однако я банально не увидел в этом ничего сокрального, чтобы настолько принципиально подойти к этому вопросу, к тому же такой же или аналогичный дефолтный дескриптор есть, пожалуй, с первых версий Jetty, Tomcat (org.apache.catalina.startup.Tomcat.DefaultWebXmlListener для embedded или ${Catalina}/conf/web.xml для standalone), GlassFish (${glassfish}/domains/domain1/config/default-web.xml), JBoss (использует Tomcat), Resin (${resin}/conf/app-default.xml), Jeronimo (использует Tomcat) — то есть практика совершенно обычна для веб-серверов и смысла переписывать стандартное поведение нет. Хотя от него можно и отказаться. Также при такой конфигурации, хотя дескриптор развертывания отсутствует и не определен, определен контекст веб-сервера, поэтому необходимости использовать мавеновский плагин maven-war-plugin с параметром failOnMissingWebXml = false нет.

Чтобы статья и зависимый код были полными, приведу исходный код непрофильных ресурсов, таких как контроллер, maven-конфигурация, jsp-страница ...

Файл проекта pom.xml (структура папок проекта не стандартная — привык, не хотел менять на стандартную, далее в этом случае могли бы быть разночтения), плагины не стал включать — тема избитая и не стоит занятого места:
pom.xml
<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.idvsbruck</groupId>
	<artifactId>jetty-spring-test</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	<name>test</name>
	<description>Embedded Jetty, Spring Java-based configuration</description>

	<developers>
		<developer>
			<id>IDVsbruck</id>
		</developer>
	</developers>

	<properties>
		<out.name>${project.artifactId}</out.name>
		<out.folder>out</out.folder>
		<source.folder>source</source.folder>
		<resource.folder>resource</resource.folder>
		<spring.version>4.1.6.RELEASE</spring.version>
		<spring.security.version>4.0.0.RELEASE</spring.security.version>
		<jetty.version>9.3.0.M1</jetty.version>
	</properties>

	<build>
		<finalName>${out.name}</finalName>
		<directory>${out.folder}</directory>
		<testOutputDirectory>${out.folder}</testOutputDirectory>
		<outputDirectory>${out.folder}</outputDirectory>
		<testSourceDirectory>${source.folder}</testSourceDirectory>
		<sourceDirectory>${source.folder}</sourceDirectory>

		<resources>
			<resource>
				<directory>${resource.folder}</directory>
				<filtering>true</filtering>
			</resource>
		</resources>

		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.1</version>
				<configuration>
					<source>1.7</source>
					<target>1.7</target>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencies>
		<!-- Embedded Jetty server -->
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-server</artifactId>
			<version>${jetty.version}</version>
		</dependency>
		<!-- Web Application Context Handler -->
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-webapp</artifactId>
			<version>${jetty.version}</version>
		</dependency>
		<!-- Поддержка спецификации аннотации сервлетов, требуется только при запуске Jetty-сервера с конфигураторами -->
		<!-- Для прямого (упрощенного) запуска Jetty не требуется -->
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-annotations</artifactId>
			<version>${jetty.version}</version>
		</dependency>
		<!-- Требуется в Runtime компиляции JSP, Jetty имплементация JSP -->
		<!-- При отсутствии в проекте jsp-файлов не требуется -->
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-jsp</artifactId>
			<version>${jetty.version}</version>
		</dependency>
		<!-- Spring MVC имплементация -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<!-- Spring ORM имплементация -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-orm</artifactId>
			<version>${spring.version}</version>
		</dependency>
		<!-- Spring Security зависимости -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-core</artifactId>
			<version>${spring.security.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
			<version>${spring.security.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
			<version>${spring.security.version}</version>
		</dependency>
		<!-- Логгер -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>1.7.5</version>
		</dependency>
		<!-- Hibernate ORM библиотека -->
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-core</artifactId>
			<version>4.3.8.Final</version>
		</dependency>
		<!-- Набор валидаторов из org.hibernate.validator.constraints в дополнение к стандартным из javax.validation.constraints -->
		<!-- Использую для валидации моделей в контроллерах - автоматизированный обработчик ошибок -->
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-validator</artifactId>
			<version>5.1.3.Final</version>
		</dependency>
		<!-- Реализация DBCP от Apache -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-dbcp2</artifactId>
			<version>2.0.1</version>
		</dependency>
		<!-- Коннектор к БД MySQL (использую в проекте эту БД) -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.31</version>
		</dependency>
		<!-- Spring Security требует классы из этой библиотеки при использовании ролевых аннотаций jsr250 -->
		<dependency>
			<groupId>javax.annotation</groupId>
			<artifactId>jsr250-api</artifactId>
			<version>1.0</version>
		</dependency>
	</dependencies>
</project>

Конфигурация проекта WEB-INF/application.properties:
application.properties
base.url=/test
base.port=8080

database.url=jdbc:mysql://localhost:3306/test
database.user=root
database.password=root

log4j.rootLogger=INFO, CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

log4j.logger.org.hibernate=WARN
log4j.logger.org.springframework=WARN
log4j.logger.org.springframework.web=WARN
log4j.logger.org.springframework.security=WARN
log4j.logger.org.eclipse.jetty=WARN

log4j.logger.com.idvsbruck.test=DEBUG

Базовая страница для теста веб-проекта WEB-INF/index.jsp:
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
	<head>
		<title>Jetty-Spring :: Test</title>
	</head>
	<body>
		<div>Welcome!<br/>Project context path: ${path}</div>
	</body>
</html>

Базовый контроллер BaseController:
BaseController.java
@Controller
public class BaseController {

	@RequestMapping(value = "/**", method = RequestMethod.GET)
	public ModelAndView index(HttpSession session, HttpServletRequest request) {
		ModelAndView result = new ModelAndView("index");
		result.addObject("path", request.getContextPath());
		return result;
	}
}

  • Использую Context Path на клиетской стороне, так как для тестового проекта используется следующий адрес — localhost:8080/test, а для фреймворков, осуществляющих собственную навигацию (обычно имплементация HTML5 History API) — так называемый Routing, при прописывании маршрутов (routs) зачастую удобнее использовать абсолютный адрес; а так как клиент сам не делит requestURI на contextPath и servletPath, то удобнее просто скармливать ему его (в production будет просто "/"). Routing есть в Backbone, Angular, Ember, React (плагины) и т.д. Сейчас я использую React и столкнулся с аномалиями использования относительных путей. Адрес проекта localhost:8080/ не вызвал бы вопросов.

  • Второе примечание касается необычного маппинга в контроллере — "/**". При классическом подходе к архитертуре веб-проекта, когда при каждом переходе происходит обращение к соответствующему методу контроллера, такое указание маппинга выглядело бы абсурдно — ошибочное указание адреса всегда приводило бы к начальному адресу. Однако, как я говорил выше, я использую на клиенте фреймворки, которые используют собственную маршрутизацию, не обращаясь к серверу за каждой страницей (так называемое, одностраничное веб-приложение с динамической сменой страниц). Допустим, используется «простой» маппинг (например, {"", "/", "/index"}): клиент получает базовую страницу localhost:8080/test/, затем меняет маршрут на localhost:8080/test/about, после чего обновляет страницу в браузере… маппинга "/about" нет в контроллере, клиент получает ошибку 404. Подстановкой маршрута занимается клиентский фреймворк и в таком веб-приложении именно он отвечает за отображение ошибочного контента. Решение — либо прописывать все маршруты проекта для одного метода контроллера (ну, или более сложная логика с разными методами, когда необходимо отдать клиенту дополнительную информацию), либо предложенное решение с одним маппингом. Так как конкретизированный маппинг имеет более высокий приоритет над множественным, нет проблемы с использованием пересекающихся маппингов: к примеру, при наличии метода с маппингом "/about" и запросом типа localhost:8080/test/about будет вызван именно конкретный метод с "/about", а не "/**" — это дефолтное поведение Spring MVC, где за сравнение и выдачу единственного маппинга занимается дефолтный PathMatcherorg.springframework.util.AntPathMatcher, метод combine(String pattern1, String pattern2). Хотя, опять-таки, такие действия направлены в угоду поведению клиента — это тенденции последнего времени, когда львиную долю работы серверных MVC-фреймворков перебирает на себя клиент. Кому-то это очень не нравится (я до недавнего времени относился к этому числу), кто-то базирует почти всю работу веб-приложения только на клиенте, используя сервер только как носитель данных и адаптер безопасности — не хочу навязывать какую-либо точку зрения, просто привел пример простого и надежного «прогиба» сервера под нужды клиента.

  • Третье замечание касается файла конфигурации проекта — application.properties. Размещение файла свойств логгера log4j.properties должно быть определено classpath: поместить в root проекта, указать вручную в настройках проекта, добавить плагины в pom.xml — неважно, главное, чтобы при запуске проекта он был виден, иначе прийдется наблюдать со старта в консоли грустное сообщение:
    log4j:WARN No appenders could be found for logger (org.eclipse.jetty.util.log).log4j:WARN Please initialize the log4j system properly.log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
    Сам вопрос выеденного яйца не стоит, но новички на него натыкаются постоянно. Сделал по-своему: все свойства хранятся в одном файле, размещение которого не требует указания в classpath, и загружаются один раз при старте проекта. Необходимость и смысл очень спорны, скорее это просто решение занимательной задачки.

Конфигурирование контекстов


Контекст приложения — ApplicationContext:
ApplicationContext.java
@Configuration
@ComponentScan(basePackages = "com.idvsbruck.test")
@PropertySource({"WEB-INF/application.properties"})
public class ApplicationContext {

	// загрузка свойств проекта в ConfigurableEnvironment контекста
	// далее будет обсуждение ненужности такого шага в рамках старта embedded Jetty наряду с @PropertySource
	@Bean
	public static PropertySourcesPlaceholderConfigurer appProperty() {
		return new PropertySourcesPlaceholderConfigurer();
	}

	// загрузка локализованных сообщений, в моем случае файлы локализации клиента и обработчика ошибок моделей и исключений
	// при отсутствии таковых необходимости в этом бине нет
	@Bean(name = "messageSource")
	public MessageSource messageSource() {
		ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
		messageSource.setBasenames("WEB-INF/i18n/messages", "WEB-INF/i18n/errors");
		messageSource.setDefaultEncoding("UTF-8");
		return messageSource;
	}
}

Этот код является аналогом следующей xml-конфигурации:
application.xml
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
 	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

	<context:property-placeholder location="/WEB-INF/application.properties"/>
	<context:annotation-config/>
	<context:component-scan base-package="com.idvsbruck.test"/>

	<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
		<property name="basenames" value="/WEB-INF/i18n/messages, /WEB-INF/i18n/errors"/>
		<property name="defaultEncoding" value="UTF-8"/>
	</bean>
</beans>

Субконтекст для работы с данными PersistenceContext:
PersistenceContext.java
@Configuration
@EnableTransactionManagement
public class PersistenceContext {

	@Autowired
	private Environment environment;

	@Bean(name = "sessionFactory")
	public SessionFactory sessionFactory() throws IOException {
		final LocalSessionFactoryBean factory = new LocalSessionFactoryBean();
		factory.setDataSource(dataSource());
		factory.setHibernateProperties(hibernateProperties());
		factory.setPackagesToScan("com.idvsbruck.test.entity");
		factory.afterPropertiesSet();
		return factory.getObject();
	}

	@Bean(name = "dataSource")
	public DataSource dataSource() {
		final BasicDataSource dataSource = new BasicDataSource();
		dataSource.setDriverClassName("com.mysql.jdbc.Driver");
		dataSource.setUrl(environment.getProperty("database.url"));
		dataSource.setUsername(environment.getProperty("database.user"));
		dataSource.setPassword(environment.getProperty("database.password"));
		return dataSource;
	}

	@Bean(name = "transactionManager")
	public HibernateTransactionManager transactionManager() throws IOException {
		return new HibernateTransactionManager(sessionFactory());
	}

	private static Properties hibernateProperties() {
		Properties hibernateProperties = new Properties();
		hibernateProperties.put("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
		hibernateProperties.put("hibernate.connection.autocommit", true);
		hibernateProperties.put("hibernate.show_sql", false);
		hibernateProperties.put("hibernate.format_sql", false);
		hibernateProperties.put("hibernate.generate_statistics", false);
		hibernateProperties.put("hibernate.hbm2ddl.auto", "update");
		hibernateProperties.put("hibernate.use_sql_comments", false);
		hibernateProperties.put("hibernate.cache.use_query_cache", false);
		hibernateProperties.put("hibernate.cache.use_second_level_cache", true);
		return hibernateProperties;
	}
}

Данный код является аналогом следующей xml-конфигурации (обычно его помещают в контекст приложения):
application-persistence.xml
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:aop="http://www.springframework.org/schema/aop"
 	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

	<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
		<property name="url" value="${database.url}"/>
		<property name="username" value="${database.user}"/>
		<property name="password" value="${database.password}"/>
	</bean>

	<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
		<property name="dataSource" ref="dataSource"/>
		<property name="hibernateProperties">
			<props>
				<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
				<prop key="hibernate.connection.autocommit">true</prop>
				<prop key="hibernate.show_sql">false</prop>
				<prop key="hibernate.format_sql">false</prop>
				<prop key="hibernate.generate_statistics">false</prop>
				<prop key="hibernate.hbm2ddl.auto">update</prop>
				<prop key="hibernate.use_sql_comments">false</prop>
				<prop key="hibernate.cache.use_query_cache">false</prop>
				<prop key="hibernate.cache.use_second_level_cache">true</prop>
			</props>
		</property>
		<property name="packagesToScan" value="com.idvsbruck.test.entity"/>
	</bean>

	<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
		<property name="sessionFactory" ref="sessionFactory"/>
	</bean>

	<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

Контекст сервлета (Spring MVC context) WebContext:
WebContext.java
@EnableWebMvc
@Configuration
public class WebContext extends WebMvcConfigurerAdapter {

	@Autowired
	private MessageSource messageSource;

	@Autowired
	private SessionFactory sessionFactory;

	// стандартный обработчик статических ресурсов, установка order в -1 желательна,
	// иначе в цепочке обработки запроса также будет вызван метод контроллера с маппингом "/**" или другими перекающимися
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/javascript/share/**").addResourceLocations("/javascript/share/");
		registry.setOrder(-1);
	}

	// стандартный resolver представлений, для небольших приложений избыточен, надобности нет
	// использован конечный класс InternalResourceViewResolver, но разницы в работе с UrlBasedViewResolver не замечено
	// использование JstlView позволяет делать JSTL-инъекции в динамические страницы или фрагменты страниц
	@Bean
	public ViewResolver viewResolver(){
		InternalResourceViewResolver resolver = new InternalResourceViewResolver();
		resolver.setPrefix("/WEB-INF/views/");
		resolver.setSuffix(".jsp");
		resolver.setViewClass(JstlView.class);
		return resolver;
	}

	// очень важное переопределение метода, что позволяет использовать дефолтный сервлет веб-сервера,
	// о необходимости которого было сказано в начале публикации
	@Override
	public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
		configurer.enable();
	}

	// валидатор, используемый в проекте для валидации моделей и обработки исключений
	@Bean(name = "validator")
	public LocalValidatorFactoryBean validator() {
		LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean();
		validatorFactoryBean.setValidationMessageSource(messageSource);
		return validatorFactoryBean;
	}

	// определение валидатора в цепочке прохождения запроса в Spring MVC
	@Override
	public Validator getValidator() {
		return validator();
	}

	// бин конфигурации, расширяющий возможности работы с локализацией (на практике просто сохраняет
	// и читает куку с дефолтной для пользователя локалью)
	@Bean
	public CookieLocaleResolver localeResolver() {
		CookieLocaleResolver localeResolver = new CookieLocaleResolver();
		localeResolver.setDefaultLocale(Locale.forLanguageTag("en"));
		return localeResolver;
	}

	// набор интерцептеров, используемых в проекте - можно отказаться совсем или расширить своими
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		// интерцептор для определения локали при ее смене пользователем
		LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
		localeChangeInterceptor.setParamName("language");
		registry.addInterceptor(localeChangeInterceptor);
		// обычный OSIV интерцептор
		OpenSessionInViewInterceptor openSessionInViewInterceptor = new OpenSessionInViewInterceptor();
		openSessionInViewInterceptor.setSessionFactory(sessionFactory);
		registry.addWebRequestInterceptor(openSessionInViewInterceptor);
		// пользовательский интерцептор для шаблонных запросов
		ApiInterceptor apiInterceptor = new ApiInterceptor();
		registry.addInterceptor(apiInterceptor).addPathPatterns("/api/**");
	}
}

Использование абстрактного класса WebMvcConfigurerAdapter облегчает настройку, предоставляя набор основных конфигурируемых параметров, однако этот класс не является обязательным. Можно использовать более низкоуровневый WebMvcConfigurationSupport, можно использовать для инициализации контекста практически базовый абстрактный AbstractAnnotationConfigDispatcherServletInitializer, а можно строить конфигурацию на бинах, делая много ненужной работы.

Данный код является аналогом следующей xml-конфигурации:
springmvc-servlet.xml
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

	<mvc:resources mapping="/javascript/share/**" location="/javascript/share/" order="-1"/>

	<mvc:default-servlet-handler/>

	<mvc:annotation-driven validator="validator"/>

	<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
		<property name="prefix" value="/WEB-INF/views/"/>
		<property name="suffix" value=".jsp"/>
	</bean>

	<mvc:interceptors>
		<bean class="org.springframework.orm.hibernate4.support.OpenSessionInViewInterceptor">
			<property name="sessionFactory" ref="sessionFactory"/>
		</bean>
		<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
			<property name="paramName" value="language"/>
		</bean>
		<mvc:interceptor>
			<mvc:mapping path="/api/**"/>
			<bean class="com.idvsbruck.test.common.ApiInterceptor"/>
		</mvc:interceptor>
	</mvc:interceptors>

	<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
		<property name="defaultLocale" value="en"/>
	</bean>
</beans>

Контекст Spring Security SecutityContext. В данной публикации не рассматриваются вопросы, связанные с механизмами защиты, ролевого управления и т.д. — всего того, что предоставляет замечательный фреймворк Spring Security. Однако конфигурация будет выглядеть неполной, если обойти вниманием конфигурирование контекста Spring Security. Поэтому он будет приведен, но не будут раскрываться тонкости взаимодействия с фрейморком и нюансы использования клиента с собственным маршрутизатором.
SecurityContext.java
@Configuration
// возможен параметр debug=true, но рекомендуется только для разработки - поток логов зашкаливает
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityContext extends WebSecurityConfigurerAdapter {

	// конфигурация web based security для конкретных http-запросов
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests().antMatchers("/**").permitAll();
		http.formLogin().loginPage("/signin");
		http.logout().invalidateHttpSession(true).logoutSuccessUrl("/").logoutUrl("/signout");
		http.anonymous().authorities("USER_ANONYMOUS").principal("guest").key("foobar");
	}

	// настройка фильтра запросов
	@Override
	public void configure(WebSecurity web) {
		web.ignoring().antMatchers("/javascript/share/**");
	}

	// установка провайдера авторизации (может быть примитивная - InMemory, или на основе токенов, связанная с БД и т.д.
	// в данном случае это установка кастомного провайдера
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.authenticationProvider(authenticationProvider());
	}

	// требование конфигуратора, без определения менеджера вылетает исключение; базовое поведение
	@Bean
	@Override
	public AuthenticationManager authenticationManager() throws Exception {
		return super.authenticationManager();
	}

	// бин кастомного провайдера
	@Bean(name = "authenticationProvider")
	public AuthenticationProvider authenticationProvider() {
		return new CustomAuthenticationProvider();
	}

	// бин кастомного UserDetailsService
	@Bean(name = "userDetailsService")
	public UserDetailsService userDetailsService() {
		return new CustomUserDetailsManager();
	}

	// кодер для паролей; на смену deprecated org.springframework.security.authentication.encoding.PasswordEncoder
	// относительно недавно появился новый интерфейс org.springframework.security.crypto.password.PasswordEncoder
	// можно использовать BCryptPasswordEncoder на основе хеш-функции BCrypt, или StandartPasswordEncoder, базирующийся
	// на алгоритме SHA-256 или NoOpPasswordEncoder без шифрования пароля (рекомендован для фазы разработки)
	@Bean(name = "passwordEncoder")
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

Данный код является аналогом следующей xml-конфигурации:
application-security.xml
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:security="http://www.springframework.org/schema/security"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

	<security:http pattern="/javascript/share/**" security="none"/>

	<security:global-method-security secured-annotations="enabled" jsr250-annotations="enabled" pre-post-annotations="enabled"/>

	<security:http auto-config="false" use-expressions="true" disable-url-rewriting="true" entry-point-ref="authEntryPoint">
		<security:intercept-url pattern="/**" access="permitAll"/>
		<security:form-login login-page="/signin"/>
		<security:logout invalidate-session="true" logout-success-url="/" logout-url="/signout"/>
		<security:anonymous enabled="true" username="guest" granted-authority="USER_ANONYMOUS" key="foobar"/>
	</security:http>
	
	<security:authentication-manager alias="authenticationManager">
		<security:authentication-provider ref="customAuthenticationProvider"/>
	</security:authentication-manager>

	<bean id="customAuthenticationProvider" class="com.idvsbruck.test.security.CustomAuthenticationProvider">
		<property name="userDetailsService" ref="userDetailsService"/>
	</bean>

	<bean id="userDetailsService" class="com.idvsbruck.test.security.CustomUserDetailsManager"/>

	<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
</beans>

Конфигурация embedded Jetty сервера


Схема запуска внедренного сервера стандартна: надо определить веб-контекст, сервлеты, фильтры и произвести непосредственный старт сервера. При определении класса, отвечающего за веб-контекст (для Jetty это WebAppContext), он имеет представление о стандартной для Java веб проектов папке WEB-INF, где обычно успешно находит дескриптор развертывания web.xml, и запускает веб-проект: те же фильтры, сервлеты, листенеры, контекст приложения, секъюрити контекст и т.д. Так как целью является отказ от xml-конфигурации, наш проект не имеет стандартного дескриптора развертывания web.xml, поэтому все необходимые настройки мы должны проделать, непосредственно указывая это в классе запуска (так сказать, упрощенный вариант конфигурации с непосредственным указанием необходимой конфигурации):
Main.java
public class Main {

	public static void main(String... args) throws Exception {
		Properties properties = new Properties();
		// читаем файл конфигурации в переменную типа Properties
		InputStream stream = Main.class.getResourceAsStream("/WEB-INF/application.properties");
		properties.load(stream);
		stream.close();
		// то самое непосредственное указание логгеру log4j на получение параметров из конфигурации
		PropertyConfigurator.configure(properties);

		// инициализируем веб-контекст на базе нашей Java-based конфигурации WebContext
		AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
		webContext.register(WebContext.class);
		// заполняем окружение контекста параметрами из файла конфигурации проекта
		webContext.getEnvironment().getPropertySources().addLast(new PropertiesPropertySource("applicationEnvironment", properties));

		// определяем стандартный сервлет Spring MVC
		ServletHolder servletHolder = new ServletHolder("test-dispatcher", new DispatcherServlet(webContext));
		servletHolder.setAsyncSupported(true);
		servletHolder.setInitOrder(1);

		// определяем стандартный фильтр Spring Security
		FilterHolder filterHolder = new FilterHolder(new DelegatingFilterProxy("springSecurityFilterChain"));
		filterHolder.setAsyncSupported(true);

		// определяем веб-контекст Jetty
		WebAppContext webAppContext = new WebAppContext();
		// указываем класс контекста приложения
		webAppContext.setInitParameter("contextConfigLocation", ApplicationContext.class.getName());
		// базовая папка проекта, где находится WEB-INF
		webAppContext.setResourceBase("resource");
		// назначаем стандартного слушателя, Context Path, созданные сервлет и фильтр
		webAppContext.addEventListener(new ContextLoaderListener(webContext));
		webAppContext.setContextPath(properties.getProperty("base.url"));
		webAppContext.addServlet(servletHolder, "/");
		webAppContext.addFilter(filterHolder, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR));

		// запускаем сервер
		Server server = new Server(Integer.parseInt(properties.getProperty("base.port")));
		server.setHandler(webAppContext);
		server.start();
		server.join();
	}
}

Если непосредственно заполнить окружение контекста (Environment) параметрами из конфигурации проекта, то можно в конфигураторе контекста приложения отказаться от повторной загрузки свойств (аннотация @PropertySource и бин PropertySourcesPlaceholderConfigurer appProperty()) — это избыточный код. Также загрузка где бы то ни было в приложении файла конфигурации, а с ней и использование аннотации @ Value для параметров из этой конфигурации, становятся ненужными — стоит только указать @ Autowired private Environment env, и можно пользоваться параметрами конфигурации. Лично я совсем недавно столкнулся с этой инновацией (?), и мне она показалась чрезвычайно удобной.

Также нет необходимости указывать непосредственно другие контексты или конфигураторы контекстов, так как аннотация @ComponentScan в ApplicationContext указывает конфигуратору на пакет, который должен быть просканирован на наличие компонентов Spring. Сталкиваясь с @ Configuration, Spring сам подхватывает необходимые конфигурации. Тут, кстати, нельзя не заметить некоторую «всеядность» Spring — скармливая в любой последовательности конфигураторы контекстов, он всегда выстроит нужную схему контекстов и будет их правильно использовать (возможно, за исключением случая, когда наряду с @ComponentScan указать @ Import для конфигуратора — такой контекст будет загружен дважды, это видно по логам). В настройке Jetty можно поменять местами WebContext и ApplicationContext, это не произведет совершенно никакого изменения при старте проекта.

Второй вариант запуска Jetty — это пойти по стандартной схеме запуска контекстов Spring — реализовать интерфейс WebApplicationInitializer. В документации Spring обычно так запускается проект Spring — переопределение метода onStartup абстрактного класса AbstractSecurityWebApplicationInitializer. Jetty также имеет механизм-фабрику, позволяющую определять контексты посредством указания инициализаторов. В этом случае класс запуска Jetty будет следующим:
Main.java
public class Main {

	public static void main(String... args) throws Exception {
		Properties properties = new Properties();
		InputStream stream = Main.class.getResourceAsStream("/WEB-INF/application.properties");
		properties.load(stream);
		stream.close();
		PropertyConfigurator.configure(properties);

		WebAppContext webAppContext = new WebAppContext();
		webAppContext.setResourceBase("resource");
		webAppContext.setContextPath(properties.getProperty("base.url"));
		webAppContext.setConfigurations(new Configuration[] {
			new WebXmlConfiguration(),
			new AnnotationConfiguration() {
				@Override
				public void preConfigure(WebAppContext context) {
					ClassInheritanceMap map = new ClassInheritanceMap();
					map.put(WebApplicationInitializer.class.getName(), new ConcurrentHashSet<String>() {{
						add(WebInitializer.class.getName());
						add(SecurityWebInitializer.class.getName());
					}});
					context.setAttribute(CLASS_INHERITANCE_MAP, map);
					_classInheritanceHandler = new ClassInheritanceHandler(map);
				}
			}
		});

		Server server = new Server(Integer.parseInt(properties.getProperty("base.port")));
		server.setHandler(webAppContext);
		server.start();
		server.join();
	}
}

Такой запуск сервера выглядит, на мой взгляд, элегантней, хотя и менее понятно. Так как определение контекстов в таком варианте находится за пределами границ запускаемого класса, вся эта чехарда с ручным назначением параметров конфигурации становится лишней, так как надо либо передавать их инициализатору, где уже назначать вручную (теряется простота и наглядность), либо использовать стандартный механизм @PropertySource, что является в данном случае предпочтительнее.
Без указания new WebXmlConfiguration() Jetty не будет иметь дефолтного поведения обработки запросов, о чем упоминалось в начале публикации: в этом классе происходит загрузка webdefault.xml из пакета jetty и определение дефолтных сервлетов, фильтров и кодировок.
Переопределенный метод preConfigure указывает коллекцию инициализаторов, которые необходимо запустить перед запуском сервера. Особое внимание внимание хочется обратить на класс SecurityWebInitilizer:
SecurityWebInitializer.java
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}

Все. Назначение данного класса — определение фильтра DelegatingFilterProxy с именем «springSecurityFilterChain». Это прямой аналог следующего фрагмента из web.xml:
web.xml
...
	<filter>
		<filter-name>springSecurityFilterChain</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
		<async-supported>true</async-supported>
	</filter>
	<filter-mapping>
		<filter-name>springSecurityFilterChain</filter-name>
		<url-pattern>/*</url-pattern>
		<dispatcher>REQUEST</dispatcher>
		<dispatcher>ERROR</dispatcher>
	</filter-mapping>
...

Сам AbstractSecurityWebApplicationInitializer непосредственно реализует интерфейс WebApplicationInitializer, только указывает метод onStartup как final, что запрещает нам его переопределять. Зачем так сделано, мне непонятно: казалось бы, правильнее было бы помимо такого однобокого инициализатора иметь стандартный для Spring MVC, добавляющий функциональность от Spring Security, но видимо, Spring решил предоставить разработчику такой выбор — или используй 2 стандартных, или добавляй в MVC'шный конфигуратор фильтр для Security (учитывая однородность контекстов и интерфейсов, можно делать простой копипастой). Хотя SecurityWebInitializer и выглядит несколько странно в проекте, для полноты «чистого» эксперимента оставляем… желающие избавиться от него просто дописывают WebInitializer.

Сам WebInitializer:
WebInitializer.java
public class WebInitializer extends AbstractSecurityWebApplicationInitializer {

	@Override
	public void onStartup(ServletContext servletContext) {
		AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
		applicationContext.register(ApplicationContext.class);
		servletContext.addListener(new ContextLoaderListener(applicationContext));
		ServletRegistration.Dynamic dispatcher = servletContext.addServlet("test-dispatcher", new DispatcherServlet(applicationContext));
		dispatcher.setLoadOnStartup(1);
		dispatcher.setAsyncSupported(true);
		dispatcher.addMapping("/");
	}
}

Однако, видимо, создателям такого механизма, впервые примененного в Spring MVC 3.1, показалось, что такое количество вручную прописанных действий избыточно, и уже в версии 3.2 появляется новый абстрактный класс AbstractAnnotationConfigDispatcherServletInitializer, облегчающий непростой процесс конфигурирования. Используя его, наш WebInitializer будет иметь следующий вид:
WebInitializer.java
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
	protected Class<?>[] getRootConfigClasses() {
		return new Class<?>[] {ApplicationContext.class};
	}

	@Override
	protected Class<?>[] getServletConfigClasses() {
		return null;
	}

	@Override
	protected String[] getServletMappings() {
		return new String[] {"/"};
	}

	@Override
	protected String getServletName() {
		return "test-dispatcher";
	}
}

Всю работу по созданию контекстов и сервлетов этот класс берет на себя.

Вот, в принципе, вся базовая настройка embedded Jetty + Spring MVC + Spring Security. Запускаем класс Main, тестовая страница доступна по адресу localhost:8080/test.
Поделиться публикацией

Комментарии 7

    +1
    А зачем это всё, если уже изобрели Spring Boot?
      0
      Spring Boot — замечательный продукт, но с его высоким уровнем абстракции, который, по сути, и является его преимуществом, он заточен либо под helloword, либо под хорошего специалиста: на это обычно указывают при работе с ним — для тривиальных настроек он обеспечивает набор АПИ, а шаг в сторону — приходится углубляться в дебри кода или использовать уже знакомые методики.
      Кроме этого, как было подмечено, это скорее мануал для новичков: на своем примере показал полную смену конфигурации с xml на нативную в разрезе веб-проекта. А продолжать использовать xml, частично использовать java config или полностью, или использовать Spring Boot — это каждый решает для себя сам.
      Общаясь с программистами, скажем так, невысокого уровня, могу сказать, что быстрый переход с xml-конфигурации на String Boot — это сильнейший разрыв шаблона: только на основе того, что в файл проекта подключены библиотеки, скажем, Hibernate'a или Jetty, те уже начинают использоваться проектом. Это очень необычно для Java проектов, где обычно каждый шорох надо описать, задекларировать или сконфигурировать.
        0
        Spring Boot делает ровным счётом то же самое, что вы описали в статье, но при этом позволяет использовать конфигурацию «по умолчанию» до тех пор, пока не требуется шаг в сторону. Как только возникает необходимость сделать шаг в сторону — достаточно в явном виде сконфигурировать только этот самый шаг и ничего больше.

        В обоих случаях — и у вас, и в Spring Boot, декларируется «opinionated» подход: «конфигурация — вот такая, и это — данность», но разница заключается в том, что Spring Boot — это огромное комьюнити с документацией, туториалами, статьями, примерами и видео на YouTube, а в вашем случае — эксклюзивный вариант конфигурации Spring «под себя» — с лично вашей мотивацией, вашими ценностями, и пониманием что хорошо, а что плохо.
      +1
      Непонятно, за что автора заминусовали. Нормальная статья начального уровня.
        +1
        если бы я минусил статью, то наверное из-за этого (учитывая, что это How-to):
        0) изменение структуры каталогов в pom.xml
        1) нет исходников целикового проекта для «пощупать»
        2) слишком много текста без разбивки на разделы
        3) мешанина из «избавляется от XML» и «вкорячиваем внедрённый web-сервер» — можно было бы вкорячить его и после того, как избавились от XML
        4) нет отсыла на другие хабра-статьи по аналогичной теме. Например на эту
        5) обучение «плохому» в виде смешения properties, вместо того, чтобы просто задействовать Log4jConfigListener
        6) использование смешанных настроек — часть в properties-файлах, а часть «зашито» в код. Причём настроек «одного класса»
        7) местами даны аналоги XML и Java конфигурации, а местами нет
        8) про назначение SecurityWebInitilizer как-то как бы и сказано, но лучше бы уж не говорилось, т.к. это не единственное его предназначение

        ЗЫ: поправил чуть карму, всё таки первая статья у ТС
          0
          Спасибо. Согласен. Желание написать статью появилось, но делать это «с чистого листа» — нет ))), поэтому и вся отсебятина. Однозначно правильней было бы сделать тестовый проект, а не адаптировать свой для публикации.
          Кстати, не первая. Писал пару лет назад про кастомизацию Spring Security под авторизацию через соцсети, но после был отхабрен и статья загадочным образом исчезла (((
        0
        Поправочка: persistence.xml описывает лишь контекст, а меппинги сущностей делаются в orm.xml.

        Как уже заметили, Spring Boot предлагает уже готовые связки с авто конфигурацией контекста, хотя усложняет в 10 раз что-либо изменить или добавить. Если же нужно в продакшне деплоить на сервер, то лучше использовать именно Stand-alone Jetty+Spring.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое