美文网首页互联网科技程序员
spring源码分析之freemarker整合

spring源码分析之freemarker整合

作者: JAVA高级架构开发 | 来源:发表于2019-03-25 17:01 被阅读44次

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

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

    对Java技术,架构技术感兴趣的同学,欢迎加群,一起学习,相互讨论。可以获取免费的学习资料,群号:614478470 点击加入


    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);
            }
        }
    

    对Java技术,架构技术感兴趣的同学,欢迎加群,一起学习,相互讨论。可以获取免费的学习资料,群号:614478470 点击加入
    调用子类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>
    

    对Java技术,架构技术感兴趣的同学,欢迎加群,一起学习,相互讨论。可以获取免费的学习资料,群号:614478470 点击加入

    相关文章

      网友评论

        本文标题:spring源码分析之freemarker整合

        本文链接:https://www.haomeiwen.com/subject/nldbvqtx.html