FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
FreeMarker是免费的,基于Apache许可证2.0版本发布。其模板编写为FreeMarker Template Language(FTL),属于简单、专用的语言。需要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,主要用于如何展现数据, 而在模板之外注意于要展示什么数据。
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. 小结
-
spring和freemarker的整合,需要定义两个bean:FreeMarkerViewResolver、FreeMarkerConfigurer。
-
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>
复制代码