Статьи

Включение пользовательского XML в конфигурацию Spring

Вступление

Одной из приятных недавних особенностей Spring (эпоха 2.x) является поддержка пользовательского XML. Это способ, которым Spring сам добавил все виды новых тегов, таких как <util:list> и  <mvc:annotation-driven>. Это работает довольно элегантно, и представляет собой интересную альтернативу для настройки Java с использованием XML, особенно если приложение уже использует Spring.

Я написал  пример приложения,  чтобы попытаться дать легко копируемый пример того, как это делается. В примере используются Spring и пользовательский анализатор XML для создания динамических меню Swing. Это хорошее сравнение с созданием динамических меню Swing с использованием  версии Digester, которую  я опубликовал некоторое время назад.

Конечно, это не очень хороший способ создания меню Java в целом! В большинстве приложений это может быть примером  мягкого кодирования . Это действительно имеет смысл только в приложении, где действительно важно иметь возможность добавлять или удалять меню без изменения кода Java. Так что относитесь к этому как к хорошему примеру, но, пожалуйста, не начинайте создавать свой GUI таким образом.

Spring Custom XML

Пользовательский XML работает в файле конфигурации Spring, потому что Spring может динамически проверять и анализировать XML. Чтобы сделать это, Spring сначала должен иметь возможность проверить XML, который он анализирует по схеме. Это делается путем поиска всех файлов в вызываемом classpath  META-INF/spring.schemas. Эти файлы обеспечивают местоположение в пути к классам для схемы XML, которая идет с данным пространством имен. Например, «основной» XML для Spring определен в  beansпространстве имен. META-INF/spring.schemas Файл в  spring-beans JAR имеет записи , как это:

http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springframework/beans/factory/xml/spring-beans-3.0.xsd

Поэтому, когда мы используем  beans схему в нашем Spring XML, он знает, где на пути к классам искать схему, чтобы он мог проверить этот XML.

После проверки схемы Spring должен найти «обработчик», который знает, как создавать бины Spring на основе XML. Spring находит обработчики, просматривая все файлы в вызываемом classpath  META-INF/spring.handlers. spring.handlers Файл в  spring-beans JAR имеет записи , как это:

http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler

Задача обработчика — создавать определения bean-компонентов, а не обычные объекты Java, которые будут жить как bean-компоненты в контексте приложения Spring. Это связано с тем, что Spring по-прежнему должен управлять такими вещами, как bean-компоненты, в зависимости от других bean-компонентов, что означает, что Spring должен проанализировать весь XML-код, чтобы выяснить граф зависимостей, прежде чем создавать какие-либо объекты.

Пример приложения

Наш пример приложения состоит из нескольких частей:

  1. spring.schemas И  spring.handlers файлы в  META-INF.
  2. Схема XML, определяющая, что является допустимым в нашем пользовательском пространстве имен.
  3. MenuNamespaceHandlerкласс записи, который позволяет нам регистрировать, какие XML-элементы идут с какими классами синтаксического анализатора.
  4. MenuDefinitionParserфактический анализатор XML для нашего пользовательского пространства имен XML.
  5. Обычный конфигурационный файл Spring XML, который также включает в себя наш собственный XML.
  6. Основной класс, чтобы начать все это.

Также есть Java-класс,  MenuItem который мы используем для хранения идентификатора, заголовка и любых дочерних элементов пункта меню. Он ничего не знает о Spring или XML; это просто POJO.

Определение пользовательского XML

spring.schemas Файл довольно просто. Обратите внимание, что это соответствует файлу на пути к классам; Spring не будет искать в Интернете вашу XML-схему во время выполнения.

http\://anvard.org/springxml/menu.xsd=org/anvard/springxml/menu.xsd

spring.handlers Файл также довольно просто. Это просто указывает на правильный класс обработчика:

http\://anvard.org/springxml/menu=org.anvard.springxml.MenuNamespaceHandler

Схема XML здесь опущена; это схема XML, и не нужно много говорить. Следует отметить, что он допускает произвольное вложение  <menu> элементов внутри других  <menu> элементов.

Еще один кусок шаблона; обработчик пространства имен. Поскольку наше пространство имен действительно простое и содержит только один элемент верхнего уровня ( menu), это одна строка:

public void init() {
	registerBeanDefinitionParser("menu", new MenuDefinitionParser());
}

Парсер, где это становится интересным. Парсер будет вызываться, когда Spring читает XML-файл, когда он сталкивается с элементом, который принадлежит соответствующему пространству имен. Однако он будет вызываться только для элемента верхнего уровня; мы должны обрабатывать любые вложенные элементы по мере необходимости.

protected AbstractBeanDefinition parseInternal(Element element,
		ParserContext context) {

	BeanDefinitionBuilder builder = parseItem(element);

	List<Element> childElements = DomUtils.getChildElementsByTagName(
			element, "menu");

	if (null != childElements && childElements.size() > 0) {
		ManagedList<AbstractBeanDefinition> children = new ManagedList<>(
				childElements.size());

		for (Element child : childElements) {
			children.add(parseInternal(child, context));
		}
		builder.addPropertyValue("children", children);
	}

	return builder.getBeanDefinition();
}

private BeanDefinitionBuilder parseItem(Element element) {
	BeanDefinitionBuilder builder = BeanDefinitionBuilder
			.rootBeanDefinition(MenuItem.class);

	String id = element.getAttribute("id");
	if (StringUtils.hasText(id)) {
		builder.addPropertyValue("id", id);
	}

	String title = element.getAttribute("title");
	if (StringUtils.hasText(title)) {
		builder.addPropertyValue("title", title);
	}

	String listener = element.getAttribute("listener");
	if (StringUtils.hasText(listener)) {
		builder.addPropertyReference("listener", listener);
	}

	return builder;
}

В этом случае, поскольку мы учли идею, что меню может содержать дочерние меню, мы должны обработать это здесь с некоторой рекурсией. Обратите внимание, что для каждого <menu> элемента любого уровня мы создаем отдельное определение bean-компонента Spring (это одна из целей  rootBeanDefinition() статического вызова метода). Действительно важно отметить, что при построении определения компонента мы не создаем  MenuItem объект напрямую и не устанавливаем никаких свойств напрямую. Фактически, в случае  children свойства мы даже не строим список правильного типа, так как  MenuItem класс ожидает получить список  MenuItem потомков, но мы строим список AbstractBeanDefinition. Пружина обрабатывает всю необходимую проводку, когда она на самом деле создает наши MenuList объекты, включая поиск каждой из ссылок в списке и заполнение нового списка реальными объектами.

Еще одна вещь, которая немного сбивает с толку, это то, что ссылка на один другой бин Spring использует  addPropertyReference(), тогда как используется управляемый список определений бина Spring  addPropertyValue().

Использование собственного XML

Теперь, когда эти элементы установлены, мы можем использовать пользовательский XML точно так же, как любой другой XML в файле конфигурации Spring. Например:

<?xml version="1.0" encoding="UTF-8"?>
<bean:beans xmlns="http://anvard.org/springxml/menu"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:bean="http://www.springframework.org/schema/beans"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://anvard.org/springxml/menu 
						  http://anvard.org/springxml/menu.xsd
                        http://www.springframework.org/schema/beans
                          http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/util
                          http://www.springframework.org/schema/util/spring-util-3.0.xsd">

	<bean:bean id="simpleListener" class="org.anvard.springxml.SimpleMenuItemListener" />

	<menu id="menu1" title="Parent 1">
		<menu id="menu2" title="Child 1" listener="simpleListener" />
		<menu id="menu3" title="Child 2" listener="simpleListener" />
		<menu id="menu4" title="Child 3" listener="simpleListener" />
		<menu id="menu5" title="Child 4" listener="simpleListener" />
	</menu>

	<menu id="menu6" title="Item 1" listener="simpleListener" />

	<menu id="menu7" title="Grand Parent 1">
		<menu id="menu8" title="Parent 2">
			<menu id="menu9" title="Child 5" listener="simpleListener" />
		</menu>
	</menu>
	
	<util:list id="toplevel">
		<bean:ref bean="menu1" />
		<bean:ref bean="menu6" />
		<bean:ref bean="menu7" />
	</util:list>

</bean:beans>

Обратите внимание, что мы можем сделать наш пользовательский XML пространством имен по умолчанию, поэтому нам не нужно добавлять префикс наших XML-элементов; мы также можем сделать  bean пространство имен по умолчанию, как это более типично в конфигурационном файле Spring XML. Мы можем свободно смешивать наш собственный XML со стандартным Spring XML.

Также обратите внимание, что наш пользовательский XML может делать ссылки обратно на обычные бины Spring, если мы делаем правильные вещи в нашем парсере, чтобы сделать эту работу.

Мы используем список, называемый  toplevel удобным способом поиска наиболее удаленных пунктов меню для нашей строки меню. После анализа XML все бины загружаются в контекст приложения Spring, и структура XML больше не применяется.

Использование этого файла из нашего основного класса выглядит так же, как любой код Spring:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("/menuDefinition.xml");

Все наши пункты меню доступны в контексте приложения Spring, поэтому мы могли бы  ctx.getBean("menu9") вернуться и вернуть пункт меню с заголовком «Child 5».

Вывод

Несмотря на то, что многие пользователи Spring переходят на конфигурацию на основе аннотаций, в XML все еще есть вещи, которые проще сделать, например, создать множество экземпляров класса с различными свойствами. Настраиваемое пространство имен XML — это способ сделать конфигурацию Spring XML более компактной и удобочитаемой.