Spring MVC之使用Freemarker

Freemarker是使用比较广泛的模板,本文介绍如何使用Spring集成Freemarker,并提供完整实例进行演示。代码结构如下:

代码结构

代码结构

定义Freemarker视图解析器

和其它web应用一样,我们可以在WebMvcConfigurerAdapter定义ViewResolver(视图解析器),这里通过子类WebConfig来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Configuration
@EnableWebMvc
@ComponentScan("tantanit.web")
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
}

@Bean
public ViewResolver viewResolver() {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setCache(true);
resolver.setPrefix("/WEB-INF/templates/");
resolver.setSuffix(".ftl");
resolver.setContentType("text/html; charset=UTF-8");
return resolver;
}

@Bean
public FreeMarkerConfigurer getFreemarkerConfig() throws IOException, TemplateException {
FreeMarkerConfigurer result = new FreeMarkerConfigurer();
result.setTemplateLoaderPaths("/");
return result;
}
}

其中,viewResolver定义了一个FreeMarkerViewResolver类型的解析器,并且配置模板路径、后缀,以及页面编码。同时,必须定义一个FreeMarkerConfigurer,至少指定加载路径,由于WebConfig类加了@Configuration注解,这个FreeMarkerConfigurer将会被FreeMarkerView使用以下代码从应用上下文中自动查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
//FreeMarkerView类中相关代码
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);
}
}

要使WebConfig生效,还需要实现AbstractAnnotationConfigDispatcherServletInitializer,以初始化DispatcherServlet,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}

@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}

}

DispatcherServlet是Spring MVC的核心,它负责接收request,并且决定request将转给哪个组件。您可以在我写的这篇文章《Spring MVC 的基础配置》中了解更多内容。
由于RootConfig是处理非web配置的,在这里,我们在RootConfig中排除web目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@ComponentScan(basePackages={"tantanit"},
excludeFilters={
@Filter(type=FilterType.CUSTOM, value=WebPackage.class)
})
public class RootConfig {
public static class WebPackage extends RegexPatternTypeFilter {
public WebPackage() {
super(Pattern.compile("tantanit\\.web"));
}
}
}

到这里,Spring与Freemarker的配置就完成了,下面对页面进行渲染。

渲染页面

主页的controller如下:

1
2
3
4
5
6
7
8
@Controller
public class HomeController {
@RequestMapping( value = "/",method = GET)
public String home(Model model) {
return "home";
}

}

home.ftl如下:

1
2
3
<h1>关于谈谈IT</h1>
<p>谈谈IT,是一个专注于计算机技术、互联网、搜索引擎、SEO、优秀软件、网站架设与运营的原创IT科技博客。</p>
<p>欢迎访问<a href="http://tantanit.com">tantanit.com</a></p>

渲染后效果如下:
代码结构

小结

本文介绍如何将Spring和Freemarker进行集成。本文相关的配置,在之前写的这几篇文章中有更详细的介绍:

此外,之前的这两篇文章讲解了Spring与其它几个主流模板的集成:

Spring MVC之使用Apache Tiles

有时候,一些页面会共用同样的布局,比如相同的头部菜单或者底部内容,可以将重复的内容抽取出来,写在单独的文件里,而每个页面在适当的地方引入这些文件。但是即使这样,也还是显得繁琐,而且一旦布局变化(比如头部的菜单移动到侧边栏),每个文件也都要改。而使用Apache Tiles,可以将方便地重复使用布局模板,由于布局间可以继承,对布局变化的处理也更加方便。

定义Tiles视图解析器

下面代码是继承自WebMvcConfigurerAdapter的WebConfig类,在其中定义了TilesConfigurer和TilesViewResolver的bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package tantanit.web;

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.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan("tantanit.web")
public class WebConfig extends WebMvcConfigurerAdapter {

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// TODO Auto-generated method stub
super.addResourceHandlers(registry);
}


// Tiles
@Bean
public TilesConfigurer tilesConfigurer() {
TilesConfigurer tiles = new TilesConfigurer();
tiles.setDefinitions(new String[] {
"/WEB-INF/layout/tiles.xml",
"/WEB-INF/views/**/tiles.xml"
});
tiles.setCheckRefresh(true);
return tiles;
}

@Bean
public ViewResolver viewResolver() {
return new TilesViewResolver();
}

}

上述代码中,配置TilesConfigurer,指定tiles定义文件,并指定了多个tiles定义文件。然后,定义视图解析器为TilesViewResolver。

下面让我们看一下/WEB-INF/layout/tiles.xml的内容。

tiles定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
"http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>

<definition name="base" template="/WEB-INF/layout/page.jsp">
<put-attribute name="header" value="/WEB-INF/layout/header.jsp" />
<put-attribute name="footer" value="/WEB-INF/layout/footer.jsp" />
</definition>

<definition name="home" extends="base">
<put-attribute name="body" value="/WEB-INF/views/home.jsp" />
</definition>

<definition name="about" extends="base">
<put-attribute name="body" value="/WEB-INF/views/about.jsp" />
</definition>

</tiles-definitions>

第一个definition中,定义了名称为base的模板,模板内容为page.jsp,并且指定了两个属性header和footer,内容分别为header.jsp,footer.jsp,这两个属性可以在page.jsp中使用。

第二个definition,定义了名称为home的页面,这个页面继承了base模板,同时,指定名称为body的属性值为home.jsp,该属性可以在page.jsp中使用。可以这样理解,名称为home的页面的布局已经在page.jsp中定义了,个性化的部分,只有body属性的内容。

第三个definition,定义了名称为about的页面,这个页面也是继承base模板,同时,指定名称为body的属性值为about.jsp。

这样说起来有点抽象,但看完page.jsp的内容就会比较清楚了。

渲染页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="t" %>
<html>
<head>
<title>谈谈IT</title>
</head>
<body>
<div id="header">
<t:insertAttribute name="header" />
</div>
<div id="content">
<t:insertAttribute name="body" />
</div>
<div id="footer">
<t:insertAttribute name="footer" />
</div>
</body>
</html>

page.jsp页面中,作为模板页面。定义了三个div,header,content和footer,其中,header的内容由属性header指定,footer的内容由属性footer指定,而这两个属性在名称为base的模板定义中已经分别指定为header.jsp和footer.jsp,属于每个页面共同的布局。而id为content的内容由属性body指定,这个属性由具体的页面决定,是每个页面除布局外的具体内容。

在页面渲染时,比如要渲染home.jsp,除了home.jsp外,还会根据模板定义,加上相应的布局,渲染完整的页面。

下面贴出布局和具体页面内容,并展示最终效果。
header.jsp:

1
2
3
4
5
6
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>

<a href="/">首页</a>
<a href="/about">关于谈谈IT</a>

footer.jsp:

1
2
3
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
Copyright &copy; 谈谈IT

home.jsp:

1
2
3
4
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%><head>
<h1>欢迎访问谈谈IT</h1>
<p>欢迎访问<a href="http://tantanit.com">tantanit.com</a></p>

about.jsp:

1
2
3
4
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<h1>关于谈谈IT</h1>
<p>谈谈IT,是一个专注于计算机技术、互联网、搜索引擎、SEO、优秀软件、网站架设与运营的原创IT科技博客。</p>

首页渲染效果如下:

首页渲染效果

关于页面渲染效果如下:

关于页面渲染效果

样式略丑,多包涵。

我已经将代码放在github上,欢迎下载。

Spring in Acton 4读书笔记之使用Thymeleaf

JSP的缺陷

尽管JSP历史悠久、应用广泛,但它有以下缺陷:

  • 尽管看起来像,但既不是HTML,也不是XML
  • JSP标签库使文档格式不友好
  • 如果JSP标签没有在服务端正确编译后发给浏览器,浏览器由于不理解JSP标签,渲染的结果是一个灾难
  • JSP标准和servlet耦合,所以JSP只能用于基于servlet的web应用视图。像email或者不基于servlet的web应用就无法使用JSP

由于JSP有以上缺陷,许多模板试图取代JSP,而其中Thymeleaf是一个令人兴奋的选择。Thymeleaf自然,不依赖于标签库。可以在任何欢迎HTML的地方编辑和渲染。并且由于不依赖于servlet标准,所以使用范围比JSP广。

如何配置Thymeleaf视图解析器

要在Spring中使用Thymeleaf,需要配置以下三个bean,以集成Thymeleaf和Spring:

  • ThymeleafViewResolver根据视图逻辑名称解析视图模板
  • SpringTemplateEngine处理模板并渲染结果
  • TemplateResolver加载Thymeleaf模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Bean
public ViewResolver viewResolver(
SpringTemplateEngine templateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine);
return viewResolver;
}

@Bean
public TemplateEngine templateEngine(
 TemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}

@Bean
public TemplateResolver templateResolver() {
TemplateResolver templateResolver =
new ServletContextTemplateResolver();
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML5");
return templateResolver;
}

通过以上配置,Spring MVC的controller返回的response中的视图将使用Thymeleaf解析。ThymeleafViewResolver实现了Spring MVC的ViewResolver接口,用来根据解析视图的逻辑名称,并且解析视图。

ThymeleafViewResolver中注入了TemplateEngine,TemplateEngine中注入了TemplateResolver。SpringTemplateEngine用于解析(parse)template文件,并且进行渲染,而TemplateResolver,和InternalResourceViewResolver类似,使用和前缀和后缀属性,再加上视图的逻辑名称,以确定template文件的位置。templateMode设置为HTML5表示将用于渲染HTML5文件。

定义Thymeleaf模板

Thymeleaf模板主要是html。不像JSP那样有特别的标签和标签库,而是在html中引入Thymeleaf的namespace后,通过给html的标签增加属性的方式来添加功能。

1
2
3
4
5
6
7
8
9
10
11
12
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spittr</title>
<link rel="stylesheet"
type="text/css"
th:href="@{/resources/style.css}"></link>
<h1>Welcome to Spittr</h1>
<a th:href="@{/spittles}">Spittles</a> |
<a th:href="@{/spitter/register}">Register</a>
</body>
</html>

上面的例子中使用了th:href标签,并使用表达式来表示要跳转的页面。Thymeleaf模板会解析这些标签和表达式,生成相应的html。即使解析失败,结果也只是a标签没有href属性而已。在浏览器中,只是无法点击超链接,并不会像JSP那样显示奇怪的JSP代码。

小结

本文列举了JSP的缺陷,介绍了一种新型的模板:Thymeleaf,并详细说明了如何使用Spring配置Thymeleaf视图解析器,简单讲解了Thymeleaf模板的使用。在后续文章中,将讲解其它几个模板的使用。

Spring in Acton 4读书笔记之视图解析

什么是视图解析

浏览器只识别静态的html文件。MVC中,controller并不直接生成html文件,而只负责为model填充数据,然后将model转给视图(view)。并且controller只知道视图的逻辑名称,并不负责视图的内容。这样,就将转发请求和解析视图这两件事在Spring MVC中进行了解耦。controller和view之间的耦合只在于对model中数据的定义。
由于controller只提供了视图的逻辑名称,Spring要知道怎样渲染视图,就需要视图解析器的帮助。视图解析器定义了视图模板文件所在的目录和后缀,和视图逻辑名称拼接之后,就构成了视图完整的路径。

Spring MVC中定义的视图解析器接口和视图接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale)
throws Exception;
}

public interface View {
String getContentType();
void render(Map<String, ?> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception;
}

ViewResolver解析视图名称,并返回View。View则根据model(数据)以及request渲染出response。只要实现了代码中的两个方法,就可以解析视图了。然而在Spring MVC中,一般不需要再这样做,因为Spring MVC为各种常用视图框架提供了视图解析器的实现。

视图解析器 描述
InternalResourceViewResolver 将视图解析为Web应用的内部资源(一般为JSP)
TilesViewResolver 将视图按Apache Tile定义进行解析,Tiles 2.0和Tiles 3.0分别有一个TilesViewResolver实现。
FreeMarkerViewResolver 将视图按FreeMarker模板解析
ThymeleafViewResolver 将视图按Thymeleaf模板解析
ResourceBundleViewResolver 将视图解析为ResourceBundle(一般是属性文件)
UrlBasedViewResolver 根据视图名称直接解析
VelocityViewResolver 根据Velocity模板解析
VelocityLayoutViewResolver 根据Velocity布局,找到对应定义进行解析
BeanNameViewResolver 将视图解析成Spring应用上下文中的bean,bean的id和视图一样
XmlViewResolver 解析XML,和BeanNameViewResolver类似
XsltViewResolver 解析XSLT
ContentNegotiatingViewResolver 根据视图的类型将视图转发给相应的另一个视图解析器
JasperReportsViewResolver 将视图按JasperReports定义解析
其中,InternalResourceViewResolver一般用来解析JSP,TilesViewResolver用来解析Apache Tile,FreeMarkerViewResolver用来解析FreeMarker,ThymeleafViewResolver用来解析Thymeleaf。ResourceBundleViewResolver一般用来解析属性文件。在一个项目中,可以同时使用多个视图解析器,解析不同类型的文件。

创建JSP视图

使用InternalResourceViewResolver解析jsp模板文件很简单,只需要配置前缀和后缀。

1
2
3
4
5
6
7
8
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}

经过这样配置后,home将会解析到/WEB-INF/views/home.jsp

小结

本文讲解了视图解析,以及如何创建JSP视图,将在后续文章中讲解如何解析其它视图模板。

Spring in Acton 4读书笔记之Spring MVC高级配置

上一篇文章中讲解了Spring MVC的基础配置,本文对应Spring in Action(Spring实战)第四版第七章中(7.1.1 Customizing DispatcherServlet configuration)和(7.1.2 Adding additional servlets and filters)的内容,将讲解如何自定义Spring MVC的配置。

如同在上一篇文章所介绍的,任何继承了AbstractAnnotationConfigDispatcherServletInitializer的类都会自动被用来在应用程序DispatcherServlet上下文中配置DispatcherServlet和其它应用上下文。AbstractAnnotationConfigDispatcherServletInitializer定义了三个抽象方法,getServletMappings,getServletConfigClasses以及getRootConfigClasses,分别定义默认的servlet映射、DispatcherServlet上下文配置文件以及其它(中间层和后端)应用上下文配置文件。继承了AbstractAnnotationConfigDispatcherServletInitializer的类必须实现这三个方法。

为DispatcherServlet添加自定义配置

除了以上三个一定要进行的配置外,根据使用场景的不同,有时需要进行一些其它的配置。AbstractAnnotationConfigDispatcherServletInitializer提供了一些方法,当有需要时,可以通过覆盖这些方法,提供额外的配置。比如customizeRegistration方法,AbstractAnnotationConfigDispatcherServletInitializer在servlet上下文中注册了DispatcherServlet之后,会调用customizeRegistration方法,通过覆盖customizeRegistration方法,可以为DispatcherServlet提供额外配置。比如下面的代码就是在处理类型为multipart的请求,将临时文件夹定义为/tmp/spittr/uploads。

1
2
3
4
5
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/spittr/uploads"));
}

添加自定义的servlet,filter和listener

AbstractAnnotationConfigDispatcherServletInitializer会创建DispatcherServlet和ContextLoaderListener。但是如果想要添加额外的servlet,filter和listener,需要自己再创建。幸运的是,要在web容器中添加组件,只需要创建initializer类。最简单的方式就是实现Spring的WebApplicationInitializer接口。下面的代码注册了一个新的servlet,并且为这个servlet定义了mapping。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.myapp.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import com.myapp.MyServlet;
public class MyServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
Dynamic myServlet =
servletContext.addServlet("myServlet", MyServlet.class);
myServlet.addMapping("/custom/**");
}
}

下面的代码为相应的servletContext新增了一个filter。

1
2
3
4
5
6
7
@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
javax.servlet.FilterRegistration.Dynamic filter =
servletContext.addFilter("myFilter", MyFilter.class);
filter.addMappingForUrlPatterns(null, false, "/custom/*");
}

可以看到,WebApplicationInitializer非常灵活,但是如果只是想给DispatcherServlet增加filter,可以直接使用AbstractAnnotationConfigDispatcherServletInitializer的getServletFilters方法。

1
2
3
4
@Override
protected Filter[] getServletFilters() {
return new Filter[] { new MyFilter() };
}

小结

我们可以使用AbstractAnnotationConfigDispatcherServletInitializer生成和配置DispatcherServlet以及ContextLoaderListener,以对Spring MVC进行一些配置,还可以使用WebApplicationInitializer,为web容器添加额外的servlet,filter和listener。

本文简单提及了如何处理multipart的请求,在下篇文章中,将对此进行详细讲解。

Spring MVC的基础配置

配置DispatcherServlet

DispatcherServlet是Spring MVC的核心,它负责接收request,并且决定request将转给哪个组件。历史上,包括DispatcherServlet的servlet是web.xml文件配置,而web.xml文件包含在war里。现在仍然可以用这种方式进行配置,但更好用的方式是使用java文件来配置servlet容器中的DispatcherServlet。

任何继承了AbstractAnnotationConfigDispatcherServletInitializer的类都会自动被用来在应用程序上下文中配置DispatcherServlet和Spring应用上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package spittr.config;
import org.springframework.web.servlet.support.
AbstractAnnotationConfigDispatcherServletInitializer;
public class SpittrWebAppInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
}

在Servlet 3.0环境中,容器会在classpath下查找任何实现了javax.servlet .ServletContainerInitializer接口的类,并用这些类来配置servlet容器。Spring提供了一个实现这个接口的类:SpringServletContainerInitializer,这个类会查找实现了WebApplicationInitializer接口的类,并且作为后者的代理,对servlet上下文进行配置。Spring 3.2引进了AbstractAnnotationConfigDispatcherServletInitializer,这个类实现了WebApplicationInitializer接口。在上面的程序中,由于SpittrWebAppInitializer继承了AbstractAnnotationConfigDispatcherServletInitializer,也就实现了WebApplicationInitializer接口。所以当程序在servlet 3.0容器中部署时,SpittrWebAppInitializer会被自动查找到,并且用来配置servlet上下文。

尽管名字很长,AbstractAnnotationConfigDispatcherServletInitializer很容易使用。SpittrWebAppInitializer继承AbstractAnnotationConfigDispatcherServletInitializer,并覆盖了三种方法。第一个方法,getServletMappings(),确定了DispatcherServlet将会被映射到哪些路径。例子中被映射到/,表示DispatcherServlet将会是应用程序的默认servlet,它将处理这个应用接收到的所有的请求。

两个应用程序上下文

当DispatcherServlet启动的时候,它创建并加载应用程序上下文,并且加载在配置文件中定义的bean。上面例子中的getServletMappings方法,使得DispatcherServlet将加载应用上下文,以及WebConfig这个java配置文件中加载的bean。

但在Spring的web应用程序中,有另一个应用上下文。这个上下文由ContextLoaderListener创建。

DispatcherServlet用来加载Controller,视图解析器和handler映射。而ContextLoaderListener是用来加载应用程序中的其它bean。这些bean是典型的中间层和数据层的bean,这些bean构成了应用程序的后端。AbstractAnnotationConfigDispatcherServletInitializer同时创建了DispatcherServlet和ContextLoaderListener。从getServletConfigClasses()返回的带@Configuration标签的类将为DispatcherServlet的应用上下文定义bean。从getRootConfigClasses()返回的带@Configuration标签的类将会用来配置ContextLoaderListener创建的应用上下文。

在上面的例子中,根配置定义在RootConfig中。而DispatcherServlet的配置定义在WebConfig中。

使Spring MVC生效

使Spring MVC生效的最简单方式是使用EnableWebMvc标签。

1
2
3
4
5
6
7
package spittr.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@EnableWebMvc
public class WebConfig {
}

上面的代码将启用Spring MVC,但至少留下了下面的问题:

  • 没有指定视图解析器,Spring将会使用默认的BeanNameViewResolver,这个解析器将根据id查找实现了View接口的类。
  • 没有启用组件扫描,Spring只能找到在配置文件中显式定义的controller。
  • DispatcherServlet被指定为应用程序的默认servlet,它将处理这个应用接收到的所有的请求,包括对图片、stylesheet等静态文件的请求也会被处理,而这往往是没有必要的。

为了解决这几个问题,可以将程序进行如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableWebMvc
@ComponentScan("spitter.web")
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}

@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}

首先,使用@ComponentScan标签标注了WebConfig,将会在spitter.web自动扫描组件。当然,相应的controller类也要加上@Controller标签。

其次,加了一个ViewResolver,指定在/WEB-INF/views/下查找jsp后缀的文件。

最后,这个新的WebConfig继承了WebMvcConfigurerAdapter,并且覆盖了configureDefaultServletHandling方法,通过调用参数DefaultServletHandlerConfigurer的enable方法,DispatcherServlet会将对静态资源的请求转发给servlet容器的默认servlet,DispatcherServlet本身不会尝试处理它们。

本文讲述了如何对Spring MVC进行基础配置,关于filter和multipart等可选的内容,将在后续文章进行讲解。

Spring in Action 4读书笔记之Spring MVC中请求的处理流程

我计划完成 50 到 100 篇有关 Spring 的文章,这是第十四篇。本文对应Spring in Action(Spring实战)第四版第五章的5.1.1(5.1.1 Following the life of a request),将讲述Spring MVC中请求的流程。

每当用户在浏览器点击链接或者提交表单的时候,就会生成一个request。request就像一个邮差,负责将信息从一个地方地方传递到另一个地方。

request很忙,它从离开浏览器开始,一直到跟着response返回到浏览器结束,会经过很多站,而每一站都释放一些信息,又带上一些新的信息。下图显示了在Spring MVC中,request在哪些站点停留。

Spring MVC中请求的处理流程

  1. 第一步,当request从浏览器发出时,它携带了用户的请求信息,至少会携带请求的url地址,但也可能携带其它数据,比如用户在表单中提交的信息。
    在Spring MVC中,DispatcherServlet实现了第一步的功能。和大多数基于Java的web框架一样,Spring MVC通过一个前端控制器servlet(front controller servlet)来处理请求。前端控制器是一种通用的web应用特征,使用一个单独的servlet,将请求转发给应用的其它组件,而后面的这些组件负责实际的处理。在Spring MVC中,DispatcherServlet就是这个前端控制器。
  2. 第二步,为请求找到匹配的控制器。DispatcherServlet的任务是将请求发送给Spring MVC控制器(controller)。控制器负责处理这些请求。但是一个web应用一般不会只有一个控制器,所以DispatcherServlet需要一些帮助来决定将请求发给哪个控制器。要做到这点,DispatcherServlet需要询问一个或多个handler mappings,因为handler mappings注重请求的url。
  3. 第三步,DispatcherServlet将请求发给选中的控制器,在控制器中,请求转交了用户在表单中提交的信息,并且耐心等待控制器处理这些信息。实际上,在设计得比较好的应用中,控制器自身一般很少处理业务逻辑,而是调用service来处理。
  4. 第四步,控制器或service处理业务逻辑之后,会产生一些信息,返回给用户,并且在浏览器中展示。这些信息被称为model。把这些信息直接返回给浏览器往往不够,它往往需要以用户友好的方式,比如html的方式返回。所以,需要视图,比如JSP来展示这些信息。控制器会将model打包好,并且决定要渲染model的视图名称,然后将请求,以及model和视图名称,返回给DispatcherServlet。
  5. 第五步,DispatcherServlet需要依靠视图解析器,根据视图名称,匹配具体的视图实现。需要注意的是,视图实现不一定是JSP,也可以是其它类型的模板。
  6. 第六步,视图将model中的数据进行渲染,生成需要返回给客户端的输出。
  7. 第七步,将结果返回给客户端。

以上就是Spring MVC中请求的处理流程,有哪里写得不够清楚的地方,欢迎在评论区进行讨论。

Spring in Acton 4读书笔记之使用AOP为类动态添加方法

我计划完成50到100篇有关Spring的文章,这是第十三篇。本文对应Spring in Action(Spring实战)第四版第四章中(4.3.4 Annotating introductions)的内容,将讲解如何使用标签为类动态添加方法。

一些像Ruby和Groovy这样的语言,有开放类(open classes)的概念,可以在不改变类和对象的定义的情况下,增加新的方法。不幸的是,Java没有那么动态,一旦一个类编译好之后,很难再为这个类增加功能了。

但是仔细想想,使用AOP的时候,难道不是在动态增加功能吗?虽然没有为类增加方法,但是为这些类执行时增加了一些功能性。进而,使用AOP的introduction的概念,可以给Spring的bean添加方法。

在Spring中,aspect对目标对象进行代理,并且和目标对象有相同接口。试想一下,如果不仅具有目标对象的接口,还添加了一些目标对象没有的接口,那么通过调用这个代理,就可以实现目标对象没有实现的接口了。这样,尽管没有改变目标对象的代码,却在功能上添加了方法。

工作原理如下图:
使用AOP为类动态添加方法

举个例子,假设Performance是一个接口,现在想为这个接口的所有实现,动态添加一个performEncore()方法。首先我们要定义一个包含performEncore()方法的接口,再定义一个aspect将这个接口与Performance的实例关联。

定义包含要引入的方法的接口

定义包含要引入的performEncore()方法的接口:

1
2
3
public interface Encoreable {
void performEncore();
}

定义aspect,将方法引入到实例

1
2
3
4
5
6
7
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}

在这个aspect中,没有使用常用的@Before,@After,@Around等标签,而是使用了@DeclareParents标签。它的value属性指定了哪些实例要引入方法,defaultImpl属性则指定了引入的接口(这个例子中是指Encoreable接口)的默认实现类,而在这个类(DefaultEncoreable)中有引入的方法的具体实现。此外,被引入的接口,使用static修饰。上面的语句中,concert.Performance+表示Performance的任何具体实现类,不包含Performance接口本身。在这个例子中,Performance接口的所有实现类都默认实现了Encoreable接口。所以,在使用Encoreable接口时,可以做到:既使用Performance的实现类的方法,又使用新添加的performEncore方法。至于Encoreable接口代表Performance的哪个具体实现类的实例,则可以在注入bean时决定,而注入的bean则自动有了新增加的performEncore方法,当然,从语法上看,是在使用Encoreable接口的方法。

下面的例子中,注入的bean(Performance接口的某个具体实现类的实例)执行了DefaultEncoreable的performEncore方法。

1
2
3
4
@Autowired
public void performEncore(Encoreable encoreable){
encoreable.performEncore();
}

最后,别忘了aspect自身需要生成实例,才能起作用:

装配aspect的bean

1
<bean class="concert.EncoreableIntroducer" />

装配bean的方法有很多种,这里是使用xml配置的方式。也可以使用java配置,装配bean,详见《Spring in Acton》第四版第二章《装配bean》读书笔记

Spring in Acton 4读书笔记之使用AOP监听函数的参数

在上一篇文章Spring in Action 4 读书笔记之使用标签创建 AOP中讲解了如何使用标签定义aspect,本文继续进行这部分内容。如前所述,Spring的AOP都是作用在方法级别,有时候,需要监听函数的参数,本文讲解如何根据不同的参数值,执行不同的行为。比如下面的代码,记录不同参数执行的次数。

定义aspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package soundsystem;
import java.util.HashMap;
import java.util.Map;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class TrackCounter {

private Map<Integer, Integer> trackCounts =
new HashMap<Integer, Integer>();

@Pointcut(
"execution(* soundsystem.CompactDisc.playTrack(int)) " +
"&& args(trackNumber)")
public void trackPlayed(int trackNumber) {}

@Before("trackPlayed(trackNumber)")
public void countTrack(int trackNumber) {
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount + 1);
}

public int getPlayCount(int trackNumber) {
return trackCounts.containsKey(trackNumber)
}

}

可以看到,这里是有@Pointcut标签定义了一个aspect。除了是有execution之外,还使用args指定参数为trackNumber,需要注意的是,args函数的参数名trackNumber需要和被监听的trackPlayed函数同名。countTrack函数使用@Before标签,定义了在trackPlayed方法执行前做的事。这里访问了trackPlayed方法的参数trackNumber,并且记录了该参数trackNumber被执行的次数。

定义配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@EnableAspectJAutoProxy
public class TrackCounterConfig {

@Bean
public CompactDisc sgtPeppers() {
BlankDisc cd = new BlankDisc();
cd.setTitle("Sgt. Pepper's Lonely Hearts Club Band");
cd.setArtist("The Beatles");
List<String> tracks = new ArrayList<String>();
tracks.add("Sgt. Pepper's Lonely Hearts Club Band");
tracks.add("With a Little Help from My Friends");
tracks.add("Lucy in the Sky with Diamonds");
tracks.add("Getting Better");
tracks.add("Fixing a Hole");
// ...other tracks omitted for brevity...
cd.setTracks(tracks);
return cd;
}

@Bean
public TrackCounter trackCounter() {
return new TrackCounter();
}
}

使用@EnableAspectJAutoProxy标签,标注配置类,表示使用代理监听的目标类。并且使用@Bean标签,生成TrackCounter这个aspect的bean以及CompactDisc的bean。

测试aspect

可以使用下面的例子,验证执行次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=TrackCounterConfig.class)
public class TrackCounterTest {
@Rule
public final StandardOutputStreamLog log = new StandardOutputStreamLog();
@Autowired
private CompactDisc cd;
@Autowired
private TrackCounter counter;
@Test
public void testTrackCounter() {
cd.playTrack(1);
cd.playTrack(2);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(3);
cd.playTrack(7);
cd.playTrack(7);
assertEquals(1, counter.getPlayCount(1));
assertEquals(1, counter.getPlayCount(2));
assertEquals(4, counter.getPlayCount(3));
assertEquals(0, counter.getPlayCount(4));
assertEquals(0, counter.getPlayCount(5));
assertEquals(0, counter.getPlayCount(6));
assertEquals(2, counter.getPlayCount(7));
}
}

Spring in Action 4读书笔记之使用标签创建AOP

在之前的读书笔记Spring in Acton 4读书笔记之AOP原理及Spring对AOP的支持中,讲到Spring对AOP的支持包含四方面:

  1. Spring基于代理的经典的AOP
  2. 使用XML配置将纯POJO转化为aspect
  3. 使用Spring的@Aspect标签,创建aspect
  4. Spring不创建aspect,只注入(引用)AspectJ框架的aspect

Spring in Action(Spring实战)的第四章第三节(4.3 Creating annotated aspects)讲述了其中第三种,即如何使用标签创建aspect。本文讲解其中的前面两小节:定义aspect以及创建around advice。

AspectJ 5引进的主要特性是使用标签创建aspect。AspectJ 5的缺点是需要学习扩展的java语言。但是AspectJ面向标签的编程模式使得将一个类转换成aspect变得很容易,只需要在类中加一些标签。

定义一个aspect

以下是使用标签创建AOP的例子,这个例子在之前的文章中有提到过,观众在演出开始前后,以及出问题时,会自动做出一些反应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class Audience {
@Before("execution(** concert.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
@Before("execution(** concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("Taking seats");
}
@AfterReturning("execution(** concert.Performance.perform(..))")
public void applause() {
 System.out.println("CLAP CLAP CLAP!!!");
}
@AfterThrowing("execution(** concert.Performance.perform(..))")
public void demandRefund() {
System.out.println("Demanding a refund");
}
}

Audience类上加上@Aspect,用来表示Audience是一个aspect,而Audience被标注的方法定义了具体的行为。在表演开始前,观众需要就座(takeSeats()),将手机静音(silenceCellPhones())。

表演结束后,观众需要鼓掌(applause()),如果表演过程中出现了异常,观众会要求退票(demandRefund())。AspectJ提供了五个标签来定义advice:

  • @After,在方法正常执行结束,或者出现异常的时候,执行aspect。
  • @AfterReturning,在方法正常执行结束后,执行aspect。
  • @AfterThrowing,在方法抛出异常的时候,执行aspect。
  • @Around,在方法执行过程中,执行aspect。
  • @Before,在方法执行之前,执行aspect。

上面的例子中,所有标签的值都是一个pointcut表达式,而且在这个例子里,正好是一样的(因为是作用在同一个方法上)。实际上,可以将这个pointcut定义好,然后进行引用,这样可以避免重复编写pointcut。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Aspect
public class Audience {
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance() {}

 @Before("performance()")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
@Before("performance()")
public void takeSeats() {
System.out.println("Taking seats");
}
@AfterReturning("performance()")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
@AfterThrowing("performance()")
public void demandRefund() {
System.out.println("Demanding a refund");
}
}

上面的代码,使用@Pointcut标签对performance()方法进行标注,这样,就可以直接使用performance()来代替pointcut表达式了。performance()只是一个标记,所以方法体可以也必须是空的。

如果只是定义了上面Audience这个aspect,那么其实什么也做不了。必须有一个配置文件,指出它是一个aspect,并且解析这些标签,然后创建代理,最终将Audience转化为一个aspect。

如果是使用JavaConfig,可以在配置文件类加上@EnableAspectJAutoProxy标签,以实现自动代理。以下是示例:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
@Bean
public Audience audience() {
return new Audience();
}
}

Spring使用AspectJ进行自动代理,仅仅是使用@AspectJ的标签作为指引,而底层仍然是Spring自己的基于代理的aspect。所以,尽管使用了@AspectJ标签,Spring的AOP仍然只能作用在方法级别。如果要使用AspectJ的全部功能,就必须在运行时注入AspectJ,而不使用Spring来创建基于代理的aspect。

创建一个around advice

前面讲解了before和after的用法,由于around的用法有些不同,也更有用,所以这里单讲。先看看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
public class Audience {
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance() {}

@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
jp.proceed();
System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable e) {
System.out.println("Demanding a refund");
}
}
}

使用@Around标签标注了watchPerformance方法,监听performance()代表的joinpoint。此时,watchPerformance的参数ProceedingJoinPoint就是指这个joinpoint。可以看到,在jp.proceed()的前后各有一些操作,甚至在抛出异常时,也有一些处理。所以,这个方法同时实现@Before、@AfterReturning和@AfterThrowing等标签的功能,更加灵活。

需要注意的是,必须执行joinpoint的proceed()方法,否则,会导致被监听的方法没有执行。

我计划完成50到100篇关于Spring的文章,这是第十一篇,欢迎订阅tantanit.com,第一时间获取文章更新,本文链接地址:http://tantanit.com/springinaction4-du-shu-bi-ji-zhi-shi-yong-biao-qian-chuang-jian-aop/

© 2022 谈谈IT All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero