spring原始碼分析之freemarker整合

JAVA架構開發發表於2019-03-25

FreeMarker是一款模板引擎: 即一種基於模板和要改變的資料, 並用來生成輸出文字(HTML網頁、電子郵件、配置檔案、原始碼等)的通用工具。 它不是面向終端使用者的,而是一個Java類庫,是一款程式設計師可以嵌入他們所開發產品的元件。

  FreeMarker是免費的,基於Apache許可證2.0版本釋出。其模板編寫為FreeMarker Template Language(FTL),屬於簡單、專用的語言。需要準備資料在真實程式語言中來顯示,比如資料庫查詢和業務運算, 之後模板顯示已經準備好的資料。在模板中,主要用於如何展現資料, 而在模板之外注意於要展示什麼資料。

spring原始碼分析之freemarker整合
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定義:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.jverstry.Configuration</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <servlet>
        <servlet-name>MyServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>MyServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file></welcome-file>
    </welcome-file-list>

</web-app>
複製程式碼

注意:上面的contextclass定義在FrameworkServlet中,contextclass設定了一個自定義的context類,且必須是WebApplicationContext的實現。

    /**
     * Set a custom context class. This class must be of type
     * {@link org.springframework.web.context.WebApplicationContext}.
     * <p>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.
     * <p>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<String, ViewResolver> matchingBeans =
                    BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
            if (!matchingBeans.isEmpty()) {
                this.viewResolvers = new ArrayList<ViewResolver>(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.
     * <p>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.
     * <p>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).
     * <p>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<String, Object> 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.
     * <p>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.
     * <p>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):
     * <ul>
     * <li>ApplicationContextAware's {@code setApplicationContext}
     * <li>InitializingBean's {@code afterPropertiesSet}
     * </ul>
     * @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<String, ?> 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<String, ?> 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<String, Object> mergedModel = createMergedOutputModel(model, request, response);
        prepareResponse(request, response);
        renderMergedOutputModel(mergedModel, request, response);
    }
複製程式碼

呼叫子類AbstractTemplateView實現上述紅色部分

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

        if (this.exposeRequestAttributes) {
            for (Enumeration<String> 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<String> 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.
     * <p>This method can be overridden if custom behavior is needed.
     */
    @Override
    protected void renderMergedTemplateModel(
            Map<String, Object> 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.
     * <p>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.
     * <p>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.
     * <p>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<String, Object> 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.
     * <p>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. 附錄:依賴包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jverstry</groupId>
    <artifactId>spring-freemarker-integration</artifactId>
    <packaging>war</packaging>
    <version>1.0.0</version>

    <name>Spring-FreeMarker-Integration</name>

    <properties>
        <java-version>1.6</java-version>
        <spring.version>3.1.2.RELEASE</spring.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>

        <!-- FreeMarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.19</version>
        </dependency>

        <!-- CGLIB, only required and used for @Configuration usage -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib-nodep</artifactId>
            <version>2.2</version>
        </dependency>

        <!-- @Inject -->
        <dependency>
            <groupId>javax.inject</groupId>
            <artifactId>javax.inject</artifactId>
            <version>1</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java-version}</source>
                    <target>${java-version}</target>
                 <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>tomcat-maven-plugin</artifactId>
                <version>1.1</version>
                <configuration>
                    <port>8282</port>
                </configuration>
            </plugin>

        </plugins>
    </build>
</project>
複製程式碼

相關文章