FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

  FreeMarker是免费的,基于Apache许可证2.0版本发布。其模板编写为FreeMarker Template Language(FTL),属于简单、专用的语言。需要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,主要用于如何展现数据, 而在模板之外注意于要展示什么数据。

1.定义(准备工作)
freemarker整合需要定义FreeMarkerViewResolver

package com.jverstry.Configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;

@EnableWebMvc
@Configuration
@ComponentScan(basePackages = "com.jverstry")
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public ViewResolver getViewResolver() {

        FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
        resolver.setCache(false);
//        resolver.setPrefix("");
        resolver.setSuffix(".ftl");

        return resolver;

    }

    @Bean
    public FreeMarkerConfigurer getFreemarkerConfig() {

        FreeMarkerConfigurer result = new FreeMarkerConfigurer();

        result.setTemplateLoaderPath("WEB-INF/pages/");

        return result;

    }    

}

在web.xml定义:




    
        contextClass
        org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    

    
        contextConfigLocation
        com.jverstry.Configuration
    

    
        org.springframework.web.context.ContextLoaderListener
    

    
        MyServlet
        org.springframework.web.servlet.DispatcherServlet
        
            contextConfigLocation
            
        
        1
    

    
        MyServlet
        /
    

    
        
    

注意:上面的contextclass定义在FrameworkServlet中,contextclass设置了一个自定义的context类,且必须是WebApplicationContext的实现。

    /**
     * Set a custom context class. This class must be of type
     * {@link org.springframework.web.context.WebApplicationContext}.
     * 

When using the default FrameworkServlet implementation, * the context class must also implement the * {@link org.springframework.web.context.ConfigurableWebApplicationContext} * interface. * @see #createWebApplicationContext */ public void setContextClass(Class contextClass) { this.contextClass = contextClass; }

可以看出,在dispatcherServlet时定义了bean:

FreeMarkerViewResolver、
FreeMarkerConfigurer
那么在DispatcherServlet中是如何识别的呢?

/**
     * Initialize the ViewResolvers used by this class.
     * 

If no ViewResolver beans are defined in the BeanFactory for this * namespace, we default to InternalResourceViewResolver. */ private void initViewResolvers(ApplicationContext context) { this.viewResolvers = null; if (this.detectAllViewResolvers) { // Find all ViewResolvers in the ApplicationContext, including ancestor contexts. Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false); if (!matchingBeans.isEmpty()) { this.viewResolvers = new ArrayList(matchingBeans.values()); // We keep ViewResolvers in sorted order. OrderComparator.sort(this.viewResolvers); } } else { try { ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class); this.viewResolvers = Collections.singletonList(vr); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default ViewResolver later. } } // Ensure we have at least one ViewResolver, by registering // a default ViewResolver if no other resolvers are found. if (this.viewResolvers == null) { this.viewResolvers = getDefaultStrategies(context, ViewResolver.class); if (logger.isDebugEnabled()) { logger.debug("No ViewResolvers found in servlet '" + getServletName() + "': using default"); } } }

然后FreeMarkerViewResolver设置FreeMarkerView

    public FreeMarkerViewResolver() {
        setViewClass(requiredViewClass());
    }

    /**
     * Requires {@link FreeMarkerView}.
     */
    @Override
    protected Class requiredViewClass() {
        return FreeMarkerView.class;
    }

FreeMarkerView在初始化时查找
FreeMarkerConfigurer 的bean

/**
     * Invoked on startup. Looks for a single FreeMarkerConfig bean to
     * find the relevant Configuration for this factory.
     * 

Checks that the template for the default Locale can be found: * FreeMarker will check non-Locale-specific templates if a * locale-specific one is not found. * @see freemarker.cache.TemplateCache#getTemplate */ @Override protected void initServletContext(ServletContext servletContext) throws BeansException { if (getConfiguration() != null) { this.taglibFactory = new TaglibFactory(servletContext); } else { FreeMarkerConfig config = autodetectConfiguration(); setConfiguration(config.getConfiguration()); this.taglibFactory = config.getTaglibFactory(); } GenericServlet servlet = new GenericServletAdapter(); try { servlet.init(new DelegatingServletConfig()); } catch (ServletException ex) { throw new BeanInitializationException("Initialization of GenericServlet adapter failed", ex); } this.servletContextHashModel = new ServletContextHashModel(servlet, getObjectWrapper()); }

自动检测

/**
     * Autodetect a {@link FreeMarkerConfig} object via the ApplicationContext.
     * @return the Configuration instance to use for FreeMarkerViews
     * @throws BeansException if no Configuration instance could be found
     * @see #getApplicationContext
     * @see #setConfiguration
     */
    protected FreeMarkerConfig autodetectConfiguration() throws BeansException {
        try {
            return BeanFactoryUtils.beanOfTypeIncludingAncestors(
                    getApplicationContext(), FreeMarkerConfig.class, true, false);
        }
        catch (NoSuchBeanDefinitionException ex) {
            throw new ApplicationContextException(
                    "Must define a single FreeMarkerConfig bean in this web application context " +
                    "(may be inherited): FreeMarkerConfigurer is the usual implementation. " +
                    "This bean may be given any name.", ex);
        }
    }

2. 渲染视图整个过程
DispatcherServlet开始

/**
     * Render the given ModelAndView.
     * 

This is the last stage in handling a request. It may involve resolving the view by name. * @param mv the ModelAndView to render * @param request current HTTP servlet request * @param response current HTTP servlet response * @throws ServletException if view is missing or cannot be resolved * @throws Exception if there's a problem rendering the view */ protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine locale for request and apply it to the response. Locale locale = this.localeResolver.resolveLocale(request); response.setLocale(locale); View view; if (mv.isReference()) { // We need to resolve the view name. view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'"); } } else { // No need to lookup: the ModelAndView object contains the actual View object. view = mv.getView(); if (view == null) { throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'"); } } // Delegate to the View object for rendering. if (logger.isDebugEnabled()) { logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'"); } try { view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'", ex); } throw ex; } }

2.1 创建视图View
如红色1所示,调用DispatcherServlet的
resolveViewName方法

/**
     * Resolve the given view name into a View object (to be rendered).
     * 

The default implementations asks all ViewResolvers of this dispatcher. * Can be overridden for custom resolution strategies, potentially based on * specific model attributes or request parameters. * @param viewName the name of the view to resolve * @param model the model to be passed to the view * @param locale the current locale * @param request current HTTP servlet request * @return the View object, or {@code null} if none found * @throws Exception if the view cannot be resolved * (typically in case of problems creating an actual View object) * @see ViewResolver#resolveViewName */ protected View resolveViewName(String viewName, Map model, Locale locale, HttpServletRequest request) throws Exception { for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } return null; }

然后调用各种的ReviewResolver来解析视图AbstractCachingViewResolver

@Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        if (!isCache()) {
            return createView(viewName, locale);
        }
        else {
            Object cacheKey = getCacheKey(viewName, locale);
            View view = this.viewAccessCache.get(cacheKey);
            if (view == null) {
                synchronized (this.viewCreationCache) {
                    view = this.viewCreationCache.get(cacheKey);
                    if (view == null) {
                        // Ask the subclass to create the View object.
                        view = createView(viewName, locale);
                        if (view == null && this.cacheUnresolved) {
                            view = UNRESOLVED_VIEW;
                        }
                        if (view != null) {
                            this.viewAccessCache.put(cacheKey, view);
                            this.viewCreationCache.put(cacheKey, view);
                            if (logger.isTraceEnabled()) {
                                logger.trace("Cached view [" + cacheKey + "]");
                            }
                        }
                    }
                }
            }
            return (view != UNRESOLVED_VIEW ? view : null);
        }
    }

调用子类UrlBasedViewResolver来创建view对象

/**
     * Overridden to implement check for "redirect:" prefix.
     * 

Not possible in {@code loadView}, since overridden * {@code loadView} versions in subclasses might rely on the * superclass always creating instances of the required view class. * @see #loadView * @see #requiredViewClass */ @Override protected View createView(String viewName, Locale locale) throws Exception { // If this resolver is not supposed to handle the given view, // return null to pass on to the next resolver in the chain. if (!canHandle(viewName, locale)) { return null; } // Check for special "redirect:" prefix. if (viewName.startsWith(REDIRECT_URL_PREFIX)) { String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); return applyLifecycleMethods(viewName, view); } // Check for special "forward:" prefix. if (viewName.startsWith(FORWARD_URL_PREFIX)) { String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); return new InternalResourceView(forwardUrl); } // Else fall back to superclass implementation: calling loadView. return super.createView(viewName, locale); }

若前缀是redirect:或者forward:则跳入相应的逻辑进行处理,否则使用父逻辑

/**
     * Create the actual View object.
     * 

The default implementation delegates to {@link #loadView}. * This can be overridden to resolve certain view names in a special fashion, * before delegating to the actual {@code loadView} implementation * provided by the subclass. * @param viewName the name of the view to retrieve * @param locale the Locale to retrieve the view for * @return the View instance, or {@code null} if not found * (optional, to allow for ViewResolver chaining) * @throws Exception if the view couldn't be resolved * @see #loadView */ protected View createView(String viewName, Locale locale) throws Exception { return loadView(viewName, locale); } /** * Delegates to {@code buildView} for creating a new instance of the * specified view class, and applies the following Spring lifecycle methods * (as supported by the generic Spring bean factory): *

    *
  • ApplicationContextAware's {@code setApplicationContext} *
  • InitializingBean's {@code afterPropertiesSet} *
* @param viewName the name of the view to retrieve * @return the View instance * @throws Exception if the view couldn't be resolved * @see #buildView(String) * @see org.springframework.context.ApplicationContextAware#setApplicationContext * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet */ @Override protected View loadView(String viewName, Locale locale) throws Exception { AbstractUrlBasedView view = buildView(viewName); View result = applyLifecycleMethods(viewName, view); return (view.checkResource(locale) ? result : null); }

2.2 渲染视图
如DispatchServlet红色部分2所示,调用View的render方法
void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception;
具体实现由AbstractView来做

/**
     * Prepares the view given the specified model, merging it with static
     * attributes and a RequestContext attribute, if necessary.
     * Delegates to renderMergedOutputModel for the actual rendering.
     * @see #renderMergedOutputModel
     */
    @Override
    public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (logger.isTraceEnabled()) {
            logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
                " and static attributes " + this.staticAttributes);
        }

        Map mergedModel = createMergedOutputModel(model, request, response);
        prepareResponse(request, response);
        renderMergedOutputModel(mergedModel, request, response);
    }

调用子类AbstractTemplateView实现上述红色部分

@Override
    protected final void renderMergedOutputModel(
            Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {

        if (this.exposeRequestAttributes) {
            for (Enumeration en = request.getAttributeNames(); en.hasMoreElements();) {
                String attribute = en.nextElement();
                if (model.containsKey(attribute) && !this.allowRequestOverride) {
                    throw new ServletException("Cannot expose request attribute '" + attribute +
                        "' because of an existing model object of the same name");
                }
                Object attributeValue = request.getAttribute(attribute);
                if (logger.isDebugEnabled()) {
                    logger.debug("Exposing request attribute '" + attribute +
                            "' with value [" + attributeValue + "] to model");
                }
                model.put(attribute, attributeValue);
            }
        }

        if (this.exposeSessionAttributes) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                for (Enumeration en = session.getAttributeNames(); en.hasMoreElements();) {
                    String attribute = en.nextElement();
                    if (model.containsKey(attribute) && !this.allowSessionOverride) {
                        throw new ServletException("Cannot expose session attribute '" + attribute +
                            "' because of an existing model object of the same name");
                    }
                    Object attributeValue = session.getAttribute(attribute);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Exposing session attribute '" + attribute +
                                "' with value [" + attributeValue + "] to model");
                    }
                    model.put(attribute, attributeValue);
                }
            }
        }

        if (this.exposeSpringMacroHelpers) {
            if (model.containsKey(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE)) {
                throw new ServletException(
                        "Cannot expose bind macro helper '" + SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE +
                        "' because of an existing model object of the same name");
            }
            // Expose RequestContext instance for Spring macros.
            model.put(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE,
                    new RequestContext(request, response, getServletContext(), model));
        }

        applyContentType(response);

        renderMergedTemplateModel(model, request, response);
    }

再调用子类FreeMarkerView实现

    /**
     * Process the model map by merging it with the FreeMarker template.
     * Output is directed to the servlet response.
     * 

This method can be overridden if custom behavior is needed. */ @Override protected void renderMergedTemplateModel( Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { exposeHelpers(model, request); doRender(model, request, response); }

然后调用doRender方法

/**
     * Render the FreeMarker view to the given response, using the given model
     * map which contains the complete template model to use.
     * 

The default implementation renders the template specified by the "url" * bean property, retrieved via {@code getTemplate}. It delegates to the * {@code processTemplate} method to merge the template instance with * the given template model. *

Adds the standard Freemarker hash models to the model: request parameters, * request, session and application (ServletContext), as well as the JSP tag * library hash model. *

Can be overridden to customize the behavior, for example to render * multiple templates into a single view. * @param model the model to use for rendering * @param request current HTTP request * @param response current servlet response * @throws IOException if the template file could not be retrieved * @throws Exception if rendering failed * @see #setUrl * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale * @see #getTemplate(java.util.Locale) * @see #processTemplate * @see freemarker.ext.servlet.FreemarkerServlet */ protected void doRender(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Expose model to JSP tags (as request attributes). exposeModelAsRequestAttributes(model, request); // Expose all standard FreeMarker hash models. SimpleHash fmModel = buildTemplateModel(model, request, response); if (logger.isDebugEnabled()) { logger.debug("Rendering FreeMarker template [" + getUrl() + "] in FreeMarkerView '" + getBeanName() + "'"); } // Grab the locale-specific version of the template. Locale locale = RequestContextUtils.getLocale(request); processTemplate(getTemplate(locale), fmModel, response); }

继续处理模板

/**
     * Process the FreeMarker template to the servlet response.
     * 

Can be overridden to customize the behavior. * @param template the template to process * @param model the model for the template * @param response servlet response (use this to get the OutputStream or Writer) * @throws IOException if the template file could not be retrieved * @throws TemplateException if thrown by FreeMarker * @see freemarker.template.Template#process(Object, java.io.Writer) */ protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response) throws IOException, TemplateException { template.process(model, response.getWriter()); }

调用freemarker jar中的freemarker.template.Template类的process方法
此过程超出spring的范围,故略去不述。

3. 小结
1.spring和freemarker的整合,需要定义两个bean:FreeMarkerViewResolver、FreeMarkerConfigurer。

2.spring在Dispatcher中定义了视图渲染的过程:创建视图,然后利用Freemarker本身提供的Template方法来处理。

处理过程中以Mode、request、response为参数。

4. 附录:依赖包



    4.0.0

    com.jverstry
    spring-freemarker-integration
    war
    1.0.0

    Spring-FreeMarker-Integration

    
        1.6
        3.1.2.RELEASE
        UTF-8
    

    

        
        
            org.springframework
            spring-context
            ${spring.version}
        
        
            org.springframework
            spring-webmvc
            ${spring.version}
        
        
            javax.servlet
            servlet-api
            2.5
            provided
        

        
        
            org.freemarker
            freemarker
            2.3.19
        

        
        
            cglib
            cglib-nodep
            2.2
        

        
        
            javax.inject
            javax.inject
            1
        

    

    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    ${java-version}
                    ${java-version}
                 ${project.build.sourceEncoding}
                
            
            
                org.codehaus.mojo
                tomcat-maven-plugin
                1.1
                
                    8282