Spring-boot: Provide a callback for customising the auto-configured FreeMarkerConfigurer

Created on 21 Apr 2017  ·  33Comments  ·  Source: spring-projects/spring-boot

boot automatic configuration freemarker lack freemarkerVariables

blocked enhancement

Most helpful comment

We feel like the properties is a little restrictive and a customizer might be a better option. We can use FreemarkerConfigurationFactory.setFreemarkerVariables().

All 33 comments

What do you mean by "freemarkerVariables"? Can you please show what you're currently having to configure that the auto-configuration should be able to configure for you?

in freemarker automatic configuration , it lack of freemarkerVariables configuration

Uploading image.png…

it lack of freemarkerVariables configuration

  freemarker:
    settings:
      object_wrapper: com.control.back.halo.basic.utils.FreemarkerObjectWrapper
      datetime_format: yyyy-MM-dd HH:mm:ss
      date_format: yyyy-MM-dd
      time_format: HH:mm:ss
@ConfigurationProperties(prefix = "spring.freemarker")
public class FreeMarkerProperties extends AbstractTemplateViewResolverProperties {   

it is no freemarkerVariables

but FreeMarkerConfigurationFactory has freemarkerVariables,i need it

I need a solution

Ok, so you'd like to be able to set the freemarkerVariables property on either the auto-configured FreeMarkerConfigurationFactoryBean or on the auto-configured FreeMarkerConfigurer bean, correct? Both are subclasses of FreeMarkerConfigurationFactory.

What variables do you want to be able to set? Can you please provide some specific examples? Please note that values other than maps, lists and scalar values are not well-suited to being set via application.properties.

shiro

shiro:
  realm: com.control.back.halo.authorization.shiro.realm.UserRealm
  login-url: /login.html
  success-url: /index.html
  unauthorized-url: /
   #filter chian
  filterChainDefinitions:  
    "/login": anon
    "/login/**": anon
    "/logout": logout
    "/css/**": anon
    "/fonts/**": anon
    "/img/**": anon
    "/js/**": anon
    "/**": authc

i use shiro ,and definition a ShiroProperties,i hope freemarkerVariables use map definition . yes?

@ConfigurationProperties(prefix = "shiro")
public class ShiroProperties {
    /**
     * Custom Realm 
     */
    private String realm;
    /**
     * URL of login
     */
    private String loginUrl;
    /**
     * URL of success
     */
    private String successUrl;
    /**
     * URL of unauthorized
     */
    private String unauthorizedUrl;
    /**
     * filter chain
     */
    private Map<String, String> filterChainDefinitions;

    /**
     * 
     */
    private String ehacheConfig;

According to the yaml's document,it can configure the map ,Why not do it

@ohalo Please take the time to format your comments as it will make them much easier to read. Also, please keep this issue focused on FreeMarker. Your latest example appears to be solely about Shiro. I still need some examples of the freemarker variables that you are trying to set.

@ConfigurationProperties(prefix = "freemarker")
public class FreeMarkerProperties extends AbstractTemplateViewResolverProperties {

    public static final String DEFAULT_TEMPLATE_LOADER_PATH = "classpath:/templates/";

    public static final String DEFAULT_PREFIX = "";

    public static final String DEFAULT_SUFFIX = ".ftl";

    /**
     * Well-known FreeMarker keys which will be passed to FreeMarker's Configuration.
     */
    private Map<String, String> settings = new HashMap<String, String>();

        private Map<String, String> freemarkerVariables = new HashMap<String, String>();

and configuration is

#freemarker
freemarker:
  settings:
    object_wrapper: com.control.back.halo.basic.utils.FreemarkerObjectWrapper
    datetime_format: yyyy-MM-dd HH:mm:ss
    date_format: yyyy-MM-dd
    time_format: HH:mm:ss    
  freemarker-variables:
    base: http://127.0.0.1:80

in FreeMarkerAutoConfiguration ,and join factory.setFreemarkerVariables(this.properties.getFreemarkerVariables());

```
protected static class FreeMarkerConfiguration {

    @Autowired
    protected FreeMarkerProperties properties;

    protected void applyProperties(FreeMarkerConfigurationFactory factory) {
        factory.setTemplateLoaderPaths(this.properties.getTemplateLoaderPath());
        factory.setPreferFileSystemAccess(this.properties.isPreferFileSystemAccess());
        factory.setDefaultEncoding(this.properties.getCharsetName());
        Properties settings = new Properties();
        settings.putAll(this.properties.getSettings());
        factory.setFreemarkerSettings(settings);
        factory.setFreemarkerVariables(this.properties.getFreemarkerVariables());
    }

}

```

Is that ok?sorry my english

The formatting is much better, thank you. Unfortunately, it's not really what it was looking for.

You've shown how what you're asking for could be implemented in Spring Boot and I already know how to do that. What I don't know is what _values_ you want to set in freemarkerVariables. You've shown one example which is setting base to http://127.0.0.1:80. What other variables would you like to be able to set and what values might they have? I am asking because the values in freemarkerVariables can be anything (it's a Map) and application.properties or application.yml aren't a good way to set some types of Object. As I said above, it's only suited to setting maps, lists and scalar values.

If you want to configure something that's more complex, or you just want to do this without an enhancement to Spring Boot, then registering a BeanPostProcessor for the FreeMarkerConfigurer or FreeMarkerConfigurationFactoryBean may be what you need to do.

ok , i understand you,because application.properties or application.yml aren't a good way to set some types of Object,thank you

because application.properties or application.yml aren't a good way to set some types of Object

Yes, that's right. Can you please share some examples of the values that you would like to set?

i want set String values ,not Object value,like: url(static resource url:img,js,css;and other system's url)

@ohalo It's still not clear what properties you're setting and what you expect to happen. I think the best way to proceed is if you could provide a small focused sample application that shows the problem.

@druidDataSource  com.alibaba.druid.pool.DruidDataSource
@driverClass com.mysql.jdbc.Driver

#freemarker
freemarker:
  settings:
    object_wrapper: com.control.back.halo.basic.utils.FreemarkerObjectWrapper
    datetime_format: yyyy-MM-dd HH:mm:ss
    date_format: yyyy-MM-dd
    time_format: HH:mm:ss
    template_update_delay : 0
  freemarker-variables:
    base: http://127.0.0.1:8080
    siteName: speedbuy 
    druidDataSource: *druidDataSource
    driverClass: *driverClass

I'm not convinced that we should support this out of the box. AFAICT most of our other template engines don't have properties that we set. Mustache does have a MustacheEnvironmentCollector which allows Spring Environment variables to be accessed, but there is no variables section in its @ConfigurationProperties.

We feel like the properties is a little restrictive and a customizer might be a better option. We can use FreemarkerConfigurationFactory.setFreemarkerVariables().

We could also consider a more general solution, for all ViewResolvers extending UrlBasedViewResolver. This class has a UrlBasedViewResolver.setAttributesMap(Map<String, Object) method that adds those attributes to all views resolved (see javadoc).

Those attributes will be available in the model when the template is being rendered.

I want the customizer. Now I implement like this (no need to custom freemarker).

public class FreeMarkerVariablesInterceptor extends HandlerInterceptorAdapter {

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {

        modelAndView.addObject("key", new Value());
    }
}

thanks

A solution:

spring:
  freemarker:
    settings:
      auto_import: "/spring.ftl as spring , /fragment/global.ftl as _global_"

global.ftl:

<#assign static_server = "http://127.0.0.1">

Use global variable:

${_global_.static_server}

Or put global variable to ServletContext

public class HomeApp extends SpringBootServletInitializer {

    @Override
    protected WebApplicationContext createRootApplicationContext
            (ServletContext servletContext) {
        WebApplicationContext ctx = super.createRootApplicationContext(servletContext);
        HomeAppConfig conf = ctx.getBean(HomeAppConfig.class);
        servletContext.setAttribute("_config_",conf);
        return ctx;
    }

}

@moqi2011
thanks

@Configuration
public class FreemarkerConfiguration extends FreeMarkerAutoConfiguration.FreeMarkerWebConfiguration {
    @Value("${ctx.static}")
    private String ctx;
    @Value("${ctx.custom}")
    private Integer custom;

    @Override
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = super.freeMarkerConfigurer();

        Map<String, Object> sharedVariables = new HashMap<>();
        if (custom != null && custom == 1) {
            sharedVariables.put("sctx", ctx);
        }
        configurer.setFreemarkerVariables(sharedVariables);
        return configurer;
    }
}

workd for me

@ohalo

Spring Boot 2.0 breaks the solution provided by @wo8335224, as FreeMarkerWebConfiguration is replaced by FreeMarkerServletWebConfiguration, which is unfortunately package-private and thus cannot be subclassed.

A currently working solution is to configure freemarker.template.Configuration bean:

@Configuration
public class FreemarkerConfig {
    public FreemarkerConfig(freemarker.template.Configuration configuration) throws TemplateModelException {
        configuration.setSharedVariable("name", "whatever type of value");
    }
}

Internally FreeMarkerConfigurer#setFreemarkerVariables delegates its work to freemarker.template.Configuration#setAllSharedVariables.

Hey @bianjp !

Yes, you can do this, but you'll lose some auto-configuration.
So better use BeanPostProcessor.

@Component
class FreeMarkerPostProcessor : BeanPostProcessor {
    override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any {
        if (bean is FreeMarkerConfigurer) {
            bean.setFreemarkerVariables(mapOf(
                    "globalVar" to "globalVarValue"
            ))
        }
        return bean
    }
}

Thanks @satahippy. Java version:

@Configuration
public class CustomFreeMarkerConfig implements BeanPostProcessor {

    Object sharedWithAllFreeMarkerTemplatesObj = new Object();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
    throws BeansException {
        if (bean instanceof FreeMarkerConfigurer) {
            FreeMarkerConfigurer configurer = (FreeMarkerConfigurer) bean;
            Map<String, Object> sharedVariables = new HashMap<>();
            sharedVariables.put("globalVar", sharedWithAllFreeMarkerTemplatesObj);
            configurer.setFreemarkerVariables(sharedVariables);
        }
        return bean;
    }
}

[edit: my original comment confused Before/After Initialization)

I feel like there should be some easier, built-in way, of doing this, but I haven't found it yet. Related question on StackOverflow: https://stackoverflow.com/questions/52730368/how-to-make-globally-shared-objects-available-to-freemarker-templates-in-spring

To be able to use FreeMarkerConfigurationFactory in a customizer, we need a framework change. I've opened https://github.com/spring-projects/spring-framework/issues/23054

Was this page helpful?
0 / 5 - 0 ratings