I am currently migrating a Spring MVC Webapp (xml-config to java-config, tomcat to embedded tomcat via spring-boot).
The webapp uses freemarker as templating engine and JSP Taglibs. Now when I call a freemarker page I get the following error:
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.)
The freemarker-header.ftl begins with following snippet:
<#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"]>
I did not find any usable search results for MetaInfTldSources and ClasspathTlds. Any one solved this problem before?
KR Habib
It is actually an easy task if you know how to do it. All you need is already embedded into FreeMarker, for instance it is TaglibFactory.ClasspathMetaInfTldSource
class. I spend several hours to investigate that problem, so I want to share a solution.
I implemented it as BeanPostProcessor
because now there is no way to set TaglibFactory
before FreeMarkerConfigurer
bean is initialized.
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;
}
}
The only restriction is that *.tld
files must have <uri>
xml tag inside. All standard spring/spring-security TLDs have it. And also these files must be inside META-INF
folder of classpath, like META-INF/mytaglib.tld
. All standard spring/spring-security TLDs are also follow this convention.
Commented line is just for example of how you can add "custom" paths of *.tld
files if for some reason you can't place them into standard location (maybe some external jar, which doesn't follow the convention). It can be extended to some sort of classpath scanning, searching for all *.tld
files and adding them into classpathTlds
. But usually it is just doesn't required if your TLDs follow JSP conventions to be placed inside META-INF
directory.
I have tested this in my FreeMarker template and it works:
<#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"]>
For custom tag ("http://my-custom-tag-library/tags") to work, it must be *.tld
file in src/main/resources/META-INF/some.tld
and it must contain the <uri>
xml tag, like <uri>http://my-custom-tag-library/tags</uri>
. It will be found by FreeMarker then.
I hope it helps someone to save several hours to find "right" solution for this problem.
Tested with spring-boot v2.0.5.RELEASE
This really should be built-in.
First, disable the built in FreeMarkerAutoConfiguration
on your Application
:
@SpringBootApplication
@EnableAutoConfiguration(exclude = {FreeMarkerAutoConfiguration.class})
public class Application extends WebMvcConfigurerAdapter {
...
]
Then add this custom configuration:
(adapted from https://github.com/isopov/fan/blob/master/fan-web/src/main/java/com/sopovs/moradanen/fan/WebApplicationConfiguration.java; Added an ObjectWrapper
to the TaglibFactory
and removed the addResourceHandlers()
override)
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;
}
}
}
Add the following to your 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>
Then you can load in your template:
<#assign s=JspTaglibs["/META-INF/spring.tld"] />
<a href="${s.mvcUrl("IC#index").build()}">Home</a>
Spring Boot doesn't support the use of JSP taglibs with Freemarker out of the box. There's an open enhancement request that you might be interested in. It contains a link to a possible workaround where you configure FreemarkerConfigurer
's tag lib factory with some additional TLDs to be loaded from the classpath:
freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(…);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With