Spring-Boot с библиотеками тегов JSP во встроенном Tomcat

В настоящее время я переношу Spring MVC Webapp (xml-config на java-config, tomcat на встроенный tomcat через spring-boot).

Веб-приложение использует freemarker в качестве механизма создания шаблонов и библиотеки тегов JSP. Теперь, когда я вызываю страницу freemarker, я получаю следующую ошибку:

freemarker.ext.jsp.TaglibFactory$TaglibGettingException: 
No TLD was found for the "http://www.springframework.org/tags/form" JSP taglib URI. (TLD-s are searched according the JSP 2.2 specification. In development- and embedded-servlet-container setups you may also need the "MetaInfTldSources" and "ClasspathTlds" freemarker.ext.servlet.FreemarkerServlet init-params or the similar system properites.)

Freemarker-header.ftl начинается со следующего фрагмента:

<#assign form=JspTaglibs["http://www.springframework.org/tags/form"]>
<#assign core=JspTaglibs["http://java.sun.com/jstl/core"]>
<#assign spring=JspTaglibs["http://www.springframework.org/tags"]>
<#assign osc=JspTaglibs["/WEB-INF/osc.tld"]>

Я не нашел никаких полезных результатов поиска для MetaInfTldSources и ClasspathTlds. Кто-нибудь решал эту проблему раньше?

К.Р. Хабиб


person Habib Pleines    schedule 17.11.2015    source источник


Ответы (5)


Spring Boot не поддерживает использование библиотек тегов JSP с Freemarker из коробки. Есть открытый запрос на улучшение, который может вас заинтересовать. Он содержит ссылка на возможный обходной путь, при котором вы настраиваете фабрику библиотеки тегов FreemarkerConfigurer с некоторыми дополнительными TLD, загружаемыми из пути к классам:

freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(…);
person Andy Wilkinson    schedule 17.11.2015
comment
Привет, я пробовал это, но это не работает ... Текущий обходной путь заключается в том, чтобы поместить tld из JAR в папку webapp / META-INF. Но это работает только при запуске приложения с помощью команды spring -boot: run. Запуск приложения через стандартный основной класс приложения в IntelliJ приводит к тому же результату, что приложение не находит файлы tld ... :-( - person Habib Pleines; 23.11.2015

Это действительно должно быть встроено.

Сначала отключите встроенный FreeMarkerAutoConfiguration на вашем Application:

@SpringBootApplication
@EnableAutoConfiguration(exclude = {FreeMarkerAutoConfiguration.class})
public class Application extends WebMvcConfigurerAdapter {
    ...
]

Затем добавьте эту настраиваемую конфигурацию:

(адаптировано из https://github.com/isopov/fan/blob/master/fan-web/src/main/java/com/sopovs/moradanen/fan/WebApplicationConfiguration.java; добавлен ObjectWrapper в TaglibFactory и удалил переопределение addResourceHandlers())

import freemarker.cache.ClassTemplateLoader;
import freemarker.ext.jsp.TaglibFactory;
import freemarker.template.TemplateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfig;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;

import javax.servlet.ServletContext;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Locale;
import java.util.Properties;

@Configuration
public class CustomFreemarkerConfiguration extends WebMvcConfigurerAdapter {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        registry.addInterceptor(localeChangeInterceptor);
    }

    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages");
        messageSource.setFallbackToSystemLocale(false);
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

    @Bean
    public SessionLocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.ENGLISH);
        return localeResolver;
    }

    @Bean
    @Autowired
    public freemarker.template.Configuration freeMarkerConfig(ServletContext servletContext) throws IOException,
            TemplateException {
        FreeMarkerConfigurer freemarkerConfig = configFreeMarkerConfigurer(servletContext);
        return freemarkerConfig.getConfiguration();
    }

    @Bean
    @Autowired
    public TaglibFactory taglibFactory(ServletContext servletContext) throws IOException, TemplateException {
        FreeMarkerConfigurer freemarkerConfig = configFreeMarkerConfigurer(servletContext);
        TaglibFactory taglibFactory = freemarkerConfig.getTaglibFactory();
        taglibFactory.setObjectWrapper(freemarker.template.Configuration.getDefaultObjectWrapper(freemarker.template.Configuration.getVersion()));
        return taglibFactory;
    }

    @Autowired
    @Bean
    public FreeMarkerConfig springFreeMarkerConfig(ServletContext servletContext) throws IOException, TemplateException {
        return new MyFreeMarkerConfig(freeMarkerConfig(servletContext), taglibFactory(servletContext));
    }

    private static FreeMarkerConfigurer configFreeMarkerConfigurer(ServletContext servletContext) throws IOException,
            TemplateException {
        FreeMarkerConfigurer freemarkerConfig = new FreeMarkerConfigurer();
        freemarkerConfig
                .setPreTemplateLoaders(new ClassTemplateLoader(CustomFreemarkerConfiguration.class, "/templates/"));
        ServletContext servletContextProxy = (ServletContext) Proxy.newProxyInstance(
                ServletContextResourceHandler.class.getClassLoader(),
                new Class<?>[] { ServletContext.class },
                new ServletContextResourceHandler(servletContext));
        freemarkerConfig.setServletContext(servletContextProxy);
        Properties settings = new Properties();
        settings.put("default_encoding", "UTF-8");
        freemarkerConfig.setFreemarkerSettings(settings);
        freemarkerConfig.afterPropertiesSet();
        return freemarkerConfig;
    }

    @Bean
    public FreeMarkerViewResolver viewResolver() {
        FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver();
        viewResolver.setCache(false);
        viewResolver.setSuffix(".ftl");
        viewResolver.setContentType("text/html;charset=UTF-8");
        return viewResolver;
    }


    private static class ServletContextResourceHandler implements InvocationHandler
    {

        private final ServletContext target;

        private ServletContextResourceHandler(ServletContext target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("getResourceAsStream".equals(method.getName())) {
                Object result = method.invoke(target, args);
                if (result == null) {
                    result = CustomFreemarkerConfiguration.class.getResourceAsStream((String) args[0]);
                }
                return result;
            } else if ("getResource".equals(method.getName())) {
                Object result = method.invoke(target, args);
                if (result == null) {
                    result = CustomFreemarkerConfiguration.class.getResource((String) args[0]);
                }
                return result;
            }

            return method.invoke(target, args);
        }
    }

    private static class MyFreeMarkerConfig implements FreeMarkerConfig {

        private final freemarker.template.Configuration configuration;
        private final TaglibFactory taglibFactory;

        private MyFreeMarkerConfig(freemarker.template.Configuration configuration, TaglibFactory taglibFactory) {
            this.configuration = configuration;
            this.taglibFactory = taglibFactory;
        }

        @Override
        public freemarker.template.Configuration getConfiguration() {
            return configuration;
        }

        @Override
        public TaglibFactory getTaglibFactory() {
            return taglibFactory;
        }
    }
}

Добавьте в свой pom.xml следующее:

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.0</version>
    </dependency>

Затем вы можете загрузить в свой шаблон:

<#assign s=JspTaglibs["/META-INF/spring.tld"] />

<a href="${s.mvcUrl("IC#index").build()}">Home</a>
person Charlie    schedule 24.01.2016
comment
Отличный ответ, полностью решает проблему. Я лично пытался добавить поддержку spring -security в мои файлы freemarker, и после добавления этой CustomFreemarkerConfiguration все, что мне нужно было сделать для этой работы, было ‹#assign security = JspTaglibs [/security.tld] /›. - person pzeszko; 02.11.2016

На самом деле это простая задача, если вы знаете, как это делать. Все, что вам нужно, уже встроено в FreeMarker, например это TaglibFactory.ClasspathMetaInfTldSource класс. Я потратил несколько часов на изучение этой проблемы, поэтому хочу поделиться решением.

Я реализовал его как BeanPostProcessor, потому что теперь нет возможности установить TaglibFactory до инициализации FreeMarkerConfigurer bean-компонента.

import freemarker.ext.jsp.TaglibFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

import java.util.Arrays;
import java.util.regex.Pattern;

/**
 * A {@link BeanPostProcessor} that enhances {@link FreeMarkerConfigurer} bean, adding
 * {@link freemarker.ext.jsp.TaglibFactory.ClasspathMetaInfTldSource} to {@code metaInfTldSources}
 * of {@link TaglibFactory}, containing in corresponding {@link FreeMarkerConfigurer} bean.
 *
 * <p>
 * This allows JSP Taglibs ({@code *.tld} files) to be found in classpath ({@code /META-INF/*.tld}) in opposition
 * to default FreeMarker behaviour, where it searches them only in ServletContext, which doesn't work
 * when we run in embedded servlet container like {@code tomcat-embed}.
 *
 * @author Ruslan Stelmachenko
 * @since 20.02.2019
 */
@Component
public class JspTagLibsFreeMarkerConfigurerBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof FreeMarkerConfigurer) {
            FreeMarkerConfigurer freeMarkerConfigurer = (FreeMarkerConfigurer) bean;
            TaglibFactory taglibFactory = freeMarkerConfigurer.getTaglibFactory();

            TaglibFactory.ClasspathMetaInfTldSource classpathMetaInfTldSource =
                    new TaglibFactory.ClasspathMetaInfTldSource(Pattern.compile(".*"));

            taglibFactory.setMetaInfTldSources(Arrays.asList(classpathMetaInfTldSource));
//            taglibFactory.setClasspathTlds(Arrays.asList("/META-INF/tld/common.tld"));
        }
        return bean;
    }
}

Единственное ограничение - файлы *.tld должны иметь внутри тег <uri> xml. Он есть во всех стандартных TLD с пружинной / пружинной защитой. А также эти файлы должны находиться внутри META-INF папки пути к классам, например META-INF/mytaglib.tld. Все стандартные TLD с пружинной / пружинной защитой также следуют этому соглашению.

Прокомментированная строка - это всего лишь пример того, как вы можете добавить «пользовательские» пути к *.tld файлам, если по какой-то причине вы не можете разместить их в стандартном месте (возможно, какой-то внешний jar, который не соответствует соглашению). Его можно расширить до некоторого вида сканирования пути к классам, поиска всех *.tld файлов и добавления их в classpathTlds. Но обычно это не требуется, если ваши TLD следуют соглашениям JSP и помещаются в каталог META-INF.

Я протестировал это в моем шаблоне FreeMarker, и он работает:

<#assign common = JspTaglibs["http://my-custom-tag-library/tags"]>
<#assign security = JspTaglibs["http://www.springframework.org/security/tags"]>
<#assign form = JspTaglibs["http://www.springframework.org/tags/form"]>
<#assign spring = JspTaglibs["http://www.springframework.org/tags"]>

Для настраиваемого тега ("http://my-custom-tag-library/tags") для работы это должен быть *.tld файл в src/main/resources/META-INF/some.tld и должен содержать тег <uri> xml, например <uri>http://my-custom-tag-library/tags</uri>. Тогда он будет найден FreeMarker.

Надеюсь, это поможет кому-то сэкономить несколько часов, чтобы найти «правильное» решение этой проблемы.

Протестировано с помощью spring -boot v2.0.5.RELEASE

person Ruslan Stelmachenko    schedule 20.02.2019

Во время рендеринга шаблона freemarker вызывает TaglibFactory, который выполняет поиск TLD четырьмя способами:

  1. addTldLocationsFromClasspathTlds
  2. addTldLocationsFromWebXml
  3. addTldLocationsFromWebInfTlds
  4. addTldLocationsFromMetaInfTlds

Все эти методы находятся в классе TablibFactory по адресу freemarker jar. Последний, просканируйте каждую банку в WEB-INF / lib в поисках /META-INF/**/*.tld. Вы можете увидеть этот журнал, если включен режим отладки для freemarker.

Посмотрите, как развертывается ваш проект. В моем случае, используя eclipse, wtp, tomcat и maven, зависимости maven были настроены в сборке Eclipse / Deployment как зависимости maven, конечно :), поэтому эти библиотеки не находятся в WEB-INF / lib и поэтому не были найдены addTldLocationsFromMetaInfTlds.

Способ решения - принудительное развертывание для копирования всех зависимостей maven в WEB-INF / lib. Я сделал это, открыв конфигурацию сервера, в eclipse view «серверы», в параметрах сервера снимите все флажки, кроме «Автоматическая перезагрузка модуля по умолчанию».

person Moesio    schedule 07.05.2016

Ни одно из этих решений не помогло мне, но после анализа обходных путей в исходном тикете Я нашел решение, которое действительно работает:

1 - Добавьте следующее в pom.xml

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.8.1</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.0</version>
    </dependency>

2 - Создайте следующие классы

2.1 ClassPathTldsLoader

import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.List;

public class ClassPathTldsLoader  {

    private static final String SECURITY_TLD = "/META-INF/security.tld";

    final private List<String> classPathTlds;

    public ClassPathTldsLoader(String... classPathTlds) {
        super();
        if(ArrayUtils.isEmpty(classPathTlds)){
            this.classPathTlds = Arrays.asList(SECURITY_TLD);
        }else{
            this.classPathTlds = Arrays.asList(classPathTlds);
        }
    }

    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;

    @PostConstruct
    public void loadClassPathTlds() {
        freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(classPathTlds);
    }


}

2.2 FreemarkerTaglibsConfig

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FreemarkerTaglibsConfig {


    @Bean
    @ConditionalOnMissingBean(ClassPathTldsLoader.class)
    public ClassPathTldsLoader classPathTldsLoader(){
        return new ClassPathTldsLoader();
    }

}

3 - Теперь вы можете загружать в ftl файлы, например, библиотеки безопасности.

<#assign spring = JspTaglibs["http://www.springframework.org/security/tags"]>

Надеюсь, это будет полезно для кого-нибудь.

person Tk421    schedule 12.05.2019