【问题标题】:Spring-Boot with JSP Tag Libs in embedded Tomcat嵌入式 Tomcat 中带有 JSP 标签库的 Spring-Boot
【发布时间】:2016-02-18 18:23:44
【问题描述】:

我目前正在迁移 Spring MVC Webapp(xml-config 到 java-config,tomcat 通过 spring-boot 到嵌入式 tomcat)。

webapp 使用 freemarker 作为模板引擎和 JSP Taglibs。现在,当我调用 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 以以下 sn-p 开头:

<#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 的可用搜索结果。以前有人解决过这个问题吗?

韩语 哈比布

【问题讨论】:

    标签: spring-boot freemarker jsp-tags embedded-tomcat-8


    【解决方案1】:

    Spring Boot 不支持通过开箱即用的 Freemarker 使用 JSP 标签库。有一个您可能感兴趣的 open enhancement request。它包含一个指向可能的解决方法的链接,您可以在其中配置 FreemarkerConfigurer 的标签库工厂,并从类路径加载一些额外的 TLD:

    freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(…);
    

    【讨论】:

    • 嗨,我试过了,但它不起作用...当前的解决方法是将 tld 从 JAR 放到 webapp/META-INF 文件夹中。但这仅在使用 spring-boot:run 命令启动应用程序时才有效。通过 IntelliJ 中的标准主应用程序类运行应用程序会导致相同的结果,即应用程序找不到 tld 文件... :-(
    【解决方案2】:

    这确实应该是内置的。

    首先,禁用Application 上的内置FreeMarkerAutoConfiguration

    @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;在TaglibFactory 中添加了ObjectWrapper 并删除了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>
    

    【讨论】:

    • 很好的答案,完全解决了问题。我个人试图为我的 freemarker 文件添加 spring-security 支持,在添加了这个 CustomFreemarkerConfiguration 之后,我要做的就是 。
    【解决方案3】:

    在渲染模板时,freemarker调用TaglibFactory,它以四种方式搜索TLD:

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

    所有这些方法都在 freemarker jar 的 TablibFactory 类中。最后一个,扫描 WEB-INF/lib 中的每个 jar,搜索 /META-INF/**/*.tld。如果启用了 freemarker 的调试模式,您可以看到此日志记录。

    看看您的项目是如何部署的。就我而言,使用 eclipse、wtp、tomcat 和 maven,maven 依赖项在 Eclipse/Deployment 程序集中配置为 maven 依赖项,当然 :),因此这些库不在 WEB-INF/lib 中,因此,未被找到addTldLocationsFromMetaInfTlds.

    一种解决方法是强制部署将所有 maven 依赖项复制到 WEB-INF/lib。我在 Eclipse 视图“服务器”中打开服务器配置,在服务器选项下取消选中所有复选框,但“默认情况下自动重新加载模块”。

    【讨论】:

      【解决方案4】:

      如果你知道怎么做,这实际上是一件容易的事。您需要的所有内容都已嵌入到 FreeMarker 中,例如它是 TaglibFactory.ClasspathMetaInfTldSource 类。我花了几个小时来调查这个问题,所以我想分享一个解决方案。

      我将它实现为BeanPostProcessor,因为现在无法在初始化FreeMarkerConfigurer bean 之前设置TaglibFactory

      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 文件里面必须有&lt;uri&gt; xml 标签。所有标准的弹簧/弹簧安全 TLD 都有它。而且这些文件必须在类路径的META-INF 文件夹中,例如META-INF/mytaglib.tld。所有标准 spring/spring-security 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”)起作用,它必须是src/main/resources/META-INF/some.tld 中的*.tld 文件,并且它必须包含&lt;uri&gt; xml 标签,例如&lt;uri&gt;http://my-custom-tag-library/tags&lt;/uri&gt;。然后它会被 FreeMarker 找到。

      我希望它可以帮助某人节省几个小时来为这个问题找到“正确”的解决方案。

      使用 spring-boot v2.0.5.RELEASE 测试

      【讨论】:

      • 这绝对应该是公认的解决方案,因为它不需要对 FreeMarker 模板进行任何更改。非常感谢@Ruslan!
      【解决方案5】:

      这些解决方案都不适合我,但在分析了 original ticket 中的解决方法后,我发现了一个可行的解决方案:

      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 类路径TldsLoader

      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"]>
      

      我希望这对其他人有用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-04-18
        • 2017-09-14
        • 1970-01-01
        • 2017-10-21
        • 2018-08-08
        • 2018-05-28
        • 1970-01-01
        相关资源
        最近更新 更多