Статьи

Интеграция весны, скорости и плитки

Мне нравится Tiles, и я много слышал о Velocity . Похоже, они служат разным целям и, по сообщениям, легко вступают в брак вместе, поэтому я решил попробовать и использовать их в веб-приложении Spring. Интеграция фактически заняла много часов и стала настоящей горкой, во время которой я многое узнал обо всех трех технологиях. Надеюсь, этот пост может пощадить кого-то в этом развлечении и позволить им сосредоточиться на бизнесе.

цели

Когда я использую плитки, мне не нравится подход по умолчанию (?) К tiles.xml . Я не хочу помещать импорт JS & CSS, заголовка страницы, навигации, тела и т. Д. Каждый в отдельный файл, как показано во фрагменте ниже, потому что это заставляет меня переключаться между окнами редактора.

1
2
3
4
5
6
<definition name='hello' template='/WEB-INF/templates/main.jsp'>
 <put-attribute name='title' value='Hello' type='string' />
 <put-attribute name='head' value='/WEB-INF/templates/hello-js-and-css.jsp' />
 <put-attribute name='nav' value='/WEB-INF/templates/hello-nav.jsp' />
 <put-attribute name='body' value='/WEB-INF/templates/hello.jsp' />
</definition>

Очевидно, я тоже не хочу помещать слишком много деталей в tiles.xml .

Что мне действительно нравится, так это наличие одного файла на страницу, сборка шаблона в одном месте, например, этот фрагмент JSP:

01
02
03
04
05
06
07
08
09
10
<tiles:insertTemplate template='template.jsp'>
 <tiles:putAttribute name='title' value='Hello' />
 <tiles:putAttribute name='head'>
  <script type='text/javascript' src='/js/jQuery.js' />
  <script type='text/javascript' src='/js/hello.js' />
 </tiles:putAttribute>
 <tiles:putAttribute name='body'>
  <div>Hello, world!</div>
 </tiles:putAttribute>
</tiles:insertTemplate>

В Velocity это должно выглядеть так :

01
02
03
04
05
06
07
08
09
10
#tiles_insertTemplate({'template': 'template.vm'})
 #tiles_putAttribute({'name':'title', 'value': 'Hello'})#end
 #tiles_putAttribute({'name':'head'})
  <script type='text/javascript' src='/js/jQuery.js' />
  <script type='text/javascript' src='/js/hello.js' />
 #end
 #tiles_putAttribute({'name':'body'})
  <div>Hello, world!</div>
 #end
#end

Тем не менее, документы по интеграции действительно предназначены для добавления некоторой поддержки Velocity в ваше приложение на основе Tiles, хотя я хотел совсем наоборот: использовать Tiles в моем многофункциональном приложении Velocity с полной поддержкой весеннего контекста, макросов и т. Д.

Решение

Короче говоря, то, что мы собираемся сделать, это:

  1. Используйте VelocityViewResolver для разрешения и рендеринга страниц.
  2. Добавить поддержку макросов Tiles в этот движок рендеринга Velocity
  3. Расширьте рендерер Tiles с полной поддержкой Velocity, включая контекст Spring, макросы и т. Д. В конечном итоге мы собираемся использовать его для оригинального движка Velocity, созданного Spring.

Полный исходный код в виде минимального, полного веб-приложения находится на github . Подробности смотрите ниже.

Весна и Скорость -> Плитка

Для первого шага мы определяем viewResolver и viewResolver следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Bean
public VelocityConfig velocityConfig() {
 VelocityConfigurer cfg = new VelocityConfigurer();
 cfg.setResourceLoaderPath('/WEB-INF/velocity/');
 cfg.setConfigLocation(context
   .getResource('/WEB-INF/velocity.properties'));
 return cfg;
}
 
@Bean
public ViewResolver viewResolver() {
 VelocityViewResolver resolver = new VelocityViewResolver();
 resolver.setViewClass(VelocityToolboxView.class);
 resolver.setSuffix('.vm');
 return resolver;
}

Важно, чтобы мы использовали VelocityToolboxView , иначе директивы плиток не будут работать.

Нам также нужно поместить в velocity.properties следующее:

01
02
03
04
05
06
07
08
09
10
userdirective=org.apache.tiles.velocity.template.AddAttributeDirective,\
  org.apache.tiles.velocity.template.AddListAttributeDirective,\
  org.apache.tiles.velocity.template.DefinitionDirective,\
  org.apache.tiles.velocity.template.GetAsStringDirective,\
  org.apache.tiles.velocity.template.ImportAttributeDirective,\
  org.apache.tiles.velocity.template.InsertAttributeDirective,\
  org.apache.tiles.velocity.template.InsertDefinitionDirective,\
  org.apache.tiles.velocity.template.InsertTemplateDirective,\
  org.apache.tiles.velocity.template.PutAttributeDirective,\
  org.apache.tiles.velocity.template.PutListAttributeDirective

Это добавляет базовую поддержку директив Tiles в Velocity, но все равно бесполезно, потому что, как только Velocity передает рендеринг в Tiles, Tiles не может рендерить Velocity и просто игнорирует его (отображая синтаксис #directives для браузера.

Плитка -> Скорость

Нам нужно научить Tiles использовать Velocity. Для этого нам понадобится пользовательский TilesInitializer :

1
2
3
4
5
6
@Bean
public TilesConfigurer tilesConfigurer() {
 TilesConfigurer cfg = new TilesConfigurer();
 cfg.setTilesInitializer(new VelocityTilesInitializer(velocityConfig()));
 return cfg;
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class VelocityTilesInitializer extends DefaultTilesInitializer {
 private VelocityConfig velocityConfig;
 
 public VelocityTilesInitializer(VelocityConfig velocityConfig) {
  this.velocityConfig = velocityConfig;
 }
 
 @Override
 protected AbstractTilesContainerFactory createContainerFactory(
   TilesApplicationContext context) {
  return new BasicTilesContainerFactory() {
 
   @Override
   protected List<TilesRequestContextFactory> getTilesRequestContextFactoriesToBeChained(
     ChainedTilesRequestContextFactory parent) {
    List<TilesRequestContextFactory> factories = super
      .getTilesRequestContextFactoriesToBeChained(parent);
    registerRequestContextFactory(
      VelocityTilesRequestContextFactory.class.getName(),
      factories, parent);
    return factories;
   }
 
   @Override
   protected AttributeRenderer createTemplateAttributeRenderer(
     BasicRendererFactory rendererFactory,
     TilesApplicationContext applicationContext,
     TilesRequestContextFactory contextFactory,
     TilesContainer container,
     AttributeEvaluatorFactory attributeEvaluatorFactory) {
    ContextPassingVelocityAttributeRenderer var = new ContextPassingVelocityAttributeRenderer(
      velocityConfig.getVelocityEngine());
    var.setApplicationContext(applicationContext);
    var.setRequestContextFactory(contextFactory);
    var.setAttributeEvaluatorFactory(attributeEvaluatorFactory);
    var.commit();
    return var;
   }
  };
 }
}

Мы почти у цели, но тут есть хитрость. Обычно в строках 31-32 вы бы поместили velocityAttributeRenderer . Однако этот рендерер полностью игнорирует контекст и механизм Spring-augmented Velocity, которые Tiles получил от Velocity. Он создает собственный VelocityEngine и позволяет ему выполнять рендеринг, отбрасывая все директивы Spring и tile и контекстные объекты.

Невозможно изменить это поведение в Tiles (что в противном случае представляется интересным исследованием шаблонов проектирования и расширяемости). Я даже создал две проблемы JIRA для этого: 541 для пересылки контекста и 542 для внедрения VelocityEngine .

Между тем, мы должны обойтись этим обходным путем ( полный ресурс см. В github ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ContextPassingVelocityAttributeRenderer extends
  AbstractTypeDetectingAttributeRenderer {
 // ...
 
 private VelocityEngine engine;
 
 public ContextPassingVelocityAttributeRenderer(VelocityEngine engine) {
  this.engine = engine;
 }
 
 // ...
 
 public void commit() {
  velocityView = new VelocityView(new TilesApplicationContextJeeConfig());
  velocityView.setVelocityEngine(engine);
 }
 
 @Override
 public void write(Object value, Attribute attribute,
   TilesRequestContext request) throws IOException {
  if (value != null) {
   if (value instanceof String) {
    InternalContextAdapter adapter = (InternalContextAdapter) ((VelocityTilesRequestContext) request)
      .getRequestObjects()[0];
    Context context = adapter.getInternalUserContext();
    Template template = velocityView.getTemplate((String) value);
    velocityView.merge(template, context, request.getWriter());
   } else {
    throw new InvalidTemplateException(
      'Cannot render a template that is not a string: '
        + value.toString());
   }
  } else {
   throw new InvalidTemplateException('Cannot render a null template');
  }
 }
 
 // ...

Он решает обе проблемы JIRA и позволяет нам достичь конечной цели:

  1. VelocityEngine внедренный в VelocityView является оригинальным VelocityEngine из Spring. Помимо прочего, он поддерживает директивы Spring и контекстно-зависимые инструменты.
  2. Метод TilesRequestContext in write прежнему содержит исходный контекст Velocity, созданный из TilesRequestContext Spring. Стандартная реализация VelocityAttributeRenderer просто выбрасывает его. Этот обходной путь выше извлекает исходный контекст и использует его для рендеринга.

Вывод

Это путешествие заняло гораздо больше времени, чем я думал. Документации по таким случаям не существует, поэтому я часами отлаживал, читал исходный код, экспериментировал, молился и ругался. Это было еще более увлекательно, так как у меня были почти нулевые знания о внутреннем разрешении и движке рендеринга Spring, а также Tiles и Velocity.

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

Обновление — Инструменты Скорости

Некоторое время спустя я обнаружил, что это решение не поддерживает свойство Velocity Tools. Вот как это сделать: Инструменты Spring & Velocity .

Ссылка: Интеграция Spring, Velocity и Tiles от нашего партнера JCG Конрада Гаруса в блоге Белки .