Spring Web MVC

全局概览

容器内部概览

嵌入式 Jetty 服务器

可以参考 spring-jetty-example。有下面几点需要注意:

  • 必须提供 SESSIONS 这个参数: new ServletContextHandler(ServletContextHandler.SESSIONS)
  • 必须添加能处理 *.jsp 的 Servlet: contextHandler.addServlet(JspServlet.class, "*.jsp")
  • jsp 页面的顶部声明必须是: <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>,而非 <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
  • 依赖的话,就是 org.eclipse.jetty:jetty-webapporg.eclipse.jetty:jetty-jsp
  • 项目结构不同。以 Maven 为例,假设项目结构为:
1
2
3
4
5
6
7
- src/main
- java
- resources
- webapp
- css
- img
- pages

那么需要将 webappresources 同时声明为资源文件夹:

1
2
3
4
5
6
7
8
9
<resources>
<resource>
<targetPath>webapp</targetPath>
<directory>src/main/webapp</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>

注: Maven 编译结束或者打包完毕后,保留 webapp 这个文件夹,而不是把 webapp 里面的所有东西放到 jar 包的根目录。而 Gradle 却不保留。所以 Gradle 要将 webapp 放在 resources 目录下面,这样打包的时候,jar 目录下面才会有 webapp 这个文件夹。

Spring Configuration 按照顺序加载

使用 @Import 即可:

1
2
3
4
@Configuration
@Import(GlobalConfig.class)
@Conditional(MonitorServerConfig.class)
public class MonitorServerConfig implements Condition {}

Spring 启动


Jetty 重要的启动类:

  • ServletContainerInitializersStarter
  • AnnotationConfiguration
  • org.eclipse.jetty.embedded.ServerWithAnnotations
  • ContainerLifeCycle

(1) 从 AbstractJettyMojo 插件开始启动:


其中默认 Configuration 类有:


(2) 当调用 start() 方法的时候:

(3) 加载 Configuration:



Jetty 哪个类扫描了 Spring 中实现了 ServletContainerInitializersStarter 接口的类:

1
org.eclipse.jetty.annotations.AnnotationConfiguration

(4) 接受客户端请求

  • jetty-server 模块

谁调用了 service 方法

1
./jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java

Jetty 相关

打成 war 包放到 webapps 目录下面即可:

1
http://hostname.com/contextPath/servletPath/pathInfo
  • If the WAR file is named myapp.war, then the context will be deployed with a context path of /myapp
  • If the WAR file is named ROOT.WAR (or any case insensitive variation), then the context will be deployed with a context path of /
  • If the WAR file is named ROOT-foobar.war ( or any case insensitive variation), then the context will be deployed with a context path of / and a virtual host of "foobar"

web.xml 启动

  • 从 Spring 3.0 开始就不需要写 WEB-INF/web.xml

运行 Spring MVC:

  • 配置 DispatcherServlet
    • web.xml
    • 3.0 之后,AbstractAnnotationConfigDispatcherServletInitializer

Any class that extends AbstractAnnotationConfigDispatcherServletInitializer will 自动 be used to configure DispatcherServlet and the Spring application context in the application’s servlet context.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这个类自动用来配置 DispatcherServlet
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
protected Class<?>[] getRootConfigClasses() {
return new Class[0];
}

// 加载 beans
protected Class<?>[] getServletConfigClasses() {
return new Class[] { WebConfig.class };
}

// DispatcherServlet 映射的多个路径
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
1
2
3
4
5
6
7
AbstractAnnotationConfigDispatcherServletInitializer
| 创
| 建
| 了
V
DispatcherServlet (getServletConfigClasses)
ContextLoaderListener (getRootConfigClasses)

ContextLoaderListener 初始化的上下文加载的 Bean 是对于整个应用程序共享的,不管是使用什么表现层技术,一般如 DAO 层、ServiceBeanDispatcherServlet 初始化的上下文加载的 Bean 是只对 Spring Web MVC 有效的 Bean,如 ControllerHandlerMappingHandlerAdapter 等等,该初始化上下文应该只加载 Web 相关组件

1
2
3
4
5
6
7
8
// 默认采用 BeanNameViewResolver (查看 Bean 的 ID)
// 默认 Component Scan 没有开启
// 默认 DispatcherServlet 接受所有请求 (图片、css等)
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.zk")
public class WebConfig extends WebMvcConfigurerAdapter {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.zk")
public class WebConfig extends WebMvcConfigurerAdapter {

// 配置一个 JSP 视图解析器
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}

// 转发静态资源请求到 Servlet 容器的默认的 Servlet
// 而不是自己去处理静态资源
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}

}

WebApplicationInitializer 启动

Servlets of release 3 can be configured programatically, without any web.xml. With Spring and its Java-configuration 创建一个配置类 that 实现 org.springframework.web.WebApplicationInitializer.

Spring will 自动找到所有实现了这个接口的类 and 运行 the according servlet contexts. More excatly its not Spring that searches for those classes, its the servlet container (e.g. jetty or tomcat ).

AbstractAnnotationConfigDispatcherServletInitializer 实现了 WebApplicationInitializer 接口。

DispatcherServlet

Spring’s web MVC framework is, like many other web MVC frameworks, request-driven, designed around a 中心 servlet that 分发请求 to controllers and offers other functionality that facilitates the development of web applications. Spring’s DispatcherServlet however, does more than just that. It is completely integrated with the Spring IoC container and as such allows you to use every other feature that Spring has.

The request processing workflow of the Spring Web MVC DispatcherServlet is illustrated in the following diagram. The pattern-savvy reader will recognize that the DispatcherServlet is an expression of the “Front Controller” design pattern (this is a pattern that Spring Web MVC shares with many other leading web frameworks).

WebApplicationInitializer 启动

Servlets of release 3 can be configured programatically, without any web.xml. With Spring and its Java-configuration you create a configuration class that implements org.springframework.web.WebApplicationInitializer.

Spring will automatically find all classes that implement this interface and start the according servlet contexts. More excatly its not Spring that searches for those classes, its the servlet container (e.g. jetty or tomcat ).

添加项目依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>

添加 Servlet 容器插件

(1) 使用 Jetty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<plugins>

<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.6.v20170531</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<webApp>
<!-- 访问路径前缀 -->
<contextPath>/</contextPath>
</webApp>
</configuration>
</plugin>

</plugins>
</build>

运行:

1
mvn jetty:run

访问 Html(Jsp) 页面

1
2
3
4
5
6
7
8
9
10
@Controller
public class HomeController {

@RequestMapping(value = "/", method = RequestMethod.GET)
public String home() {
// 返回的是 View 的名字
return "home";
}

}

对应的 home.jsp 的位置是:

1
2
3
4
5
6
├── src
│   ├── main
│   │   └── webapp
│   │   └── WEB-INF
│   │   └── views
│   │   └── home.jsp

默认解析 View 的是 BeanNameViewResolver,所以需要配置自己的 Jsp 解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebMvc
@ComponentScan()
public class WebConfig extends WebMvcConfigurerAdapter {

@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resourceViewResolver = new InternalResourceViewResolver();
resourceViewResolver.setPrefix("/WEB-INF/views/");
resourceViewResolver.setSuffix(".jsp");
resourceViewResolver.setExposeContextBeansAsAttributes(true);
return resourceViewResolver;
}

}

访问需要动态填写值的 Html(Jsp) 页面

首先添加 JSTL 依赖:

1
2
3
4
5
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>

然后使用 Model 传值给 JSP 页面:

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class HomeController {

@RequestMapping(value = "/page-with-dynamic-value", method = RequestMethod.GET)
public String pageWithDynamicValue(Model model) {
model.addAttribute("dynamicTitle", "Dynamic Title");
model.addAttribute("dynamicValue", "Dynamic Value");
return "page-with-dynamic-value";
}

}

其中 page-with-dynamic-value.jsp 内容为:

1
2
3
4
5
6
7
8
9
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<title><c:out value="${dynamicTitle}" /></title>
</head>
<body>
<c:out value="${dynamicValue}" />
</body>
</html>

注: key 的命名最好不要有特殊符号,一开始我用 dynamic-title 怎么也不出来,在这里浪费了好长时间

访问静态文件

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebMvc
@ComponentScan()
public class WebConfig extends WebMvcConfigurerAdapter {

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
// 允许静态内容访问
configurer.enable();
}

}

我们通过输入 http://localhost:8080/public/js/hello.js 来访问 hello.js

1
2
3
4
5
6
├── src
│   ├── main
│   │   └── webapp
│   │   ├── public
│   │   │   └── js
│   │   │   └── hello.js

解析请求参数

使用 @RequestParam 来读取用户输入的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class HomeController {

@RequestMapping(value = "/request-with-parameter", method = RequestMethod.GET)
public String requestWithParameter (
@RequestParam(value="resourceID", defaultValue = "100") long resourceID,
Model model
) {
model.addAttribute("resourceID", resourceID);
return "request-with-parameter";
}

}

然后使用如下 URL 来访问,可以在页面上观察到读取到的用户传入的参数:

1
http://localhost:8080/request-with-parameter?resourceID=1

注: @RequestParam 也能接受 POST 请求的参数

从路径中解析参数

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class HomeController {

@RequestMapping(value = "/request-based-path-parameter/{resourceID}", method = RequestMethod.GET)
public String requestBasedPathParameter(
@PathVariable("resourceID") long resourceID,
Model model
) {
model.addAttribute("resourceID", resourceID);
return "request-with-parameter";
}

}

然后使用如下 URL 来访问,可以在页面上观察到从路径中读取到的用户传入的参数:

1
http://localhost:8080/request-based-path-parameter/1

从表单中解析参数

表单:

1
2
3
4
5
6
7
<form action="/submit-form" method="POST">
Name:<br>
<input type="text" name="name"><br>
Password:<br>
<input type="password" name="password"><br><br>
<input type="submit" value="Submit">
</form>

注意这次用来接受表单参数的是一个 User 对象:

1
2
3
4
5
6
7
8
9
10
@Controller
public class HomeController {

@RequestMapping(value = "/submit-form", method = RequestMethod.POST)
public String handleForm(User user) {
System.out.println(user);
return "redirect:/";
}

}

User 其实就是一个简单的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {

private String name;
private String password;

public String getName() { return name; }

public void setName(String name) { this.name = name; }

public String getPassword() { return password; }

public void setPassword(String password) { this.password = password; }

}

在提交表单之后,浏览器的 URL 会自动被重定向到首页,注意浏览器的地址也会变化

注意: form 表单默认使用 GET 提交表单,所以大多数情况下都应该指定 methodPOST 提交

接受 JSON 数据

1
2
3
4
public String insert(@RequestBody String requestBody) {
net.sf.json.JSONObject jsonObject =
net.sf.json.JSONObject.fromObject(requestBody);
}

测试:

1
curl -X POST -d '{key: value}' http://localhost:8080/test.json -H 'Content-Type:application/json'
  • 一定要设置上 -H 'Content-Type:application/json' 这个头,否则服务器不认为这是一个 json 字符串

重定向的时候携带参数 - URL 拼接

直接将参数放进 URL 里面即可:

1
2
3
4
5
6
7
8
9
10
@Controller
public class HomeController {

@RequestMapping(value = "/submit-params-form", method = RequestMethod.POST)
public String handleRedirectWithParamsForm(User user, Model model) {
model.addAttribute("userName", user.getName());
return "redirect:/redirect-page/{userName}";
}

}

重定向的时候携带参数 - 使用 FlastAttribute

使用 RedirectAttributes 的方法 addFlashAttribute 可以在重定向的时候传递复杂一点的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class HomeController {

@RequestMapping(value = "/submit-flash-attribute-form", method = RequestMethod.POST)
public String handleRedirectWithFlashAttributeForm(User user, RedirectAttributes model) {
model.addFlashAttribute("user", user);
return "redirect:/redirect-flash-attribute-page";
}

@RequestMapping(value = "/redirect-flash-attribute-page", method = RequestMethod.GET)
public String redirectWithFlashAttributePage(Model model) {
if (!model.containsAttribute("user")) {
System.err.println("Fatal error!");
}
return "redirect-flash-attribute-page";
}

}

返回 JSON 数据

首先添加依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.0.pr4</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0.pr4</version>
</dependency>

然后使用 @ResponseBody 来标记返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class HomeController {

@RequestMapping(value = "/users",
method = RequestMethod.GET)
public @ResponseBody List<User> allUsers() {
List<User> users = new ArrayList<User>(2);
users.add(createUser("Tom", "123"));
users.add(createUser("Jerry", "456"));
return users;
}

}

请求:

1
curl http://localhost:8080/ucurl http://localhost:8080/users -H 'Accept: application/json'

另外一种方式是标注返回类型为 String,然后 ObjectMapperObject 转为 Json 字符串:

1
2
3
4
5
6
static final ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(map);
} catch (JsonProcessingException e) {
return e.toString();
}

返回 XML 数据

需要添加依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.0.pr4</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.0.pr4</version>
</dependency>

同样也需要使用 @ResponseBody 来标注返回结果,然后进行请求:

1
curl http://localhost:8080/ucurl http://localhost:8080/users -H 'Accept: application/xml'

返回 JSON 或者 XML 数据的注意事项

默认情况下,Spring 会根据客户端携带的 Accept 请求头进行返回格式的猜测:

1
2
curl http://localhost:8080/ucurl http://localhost:8080/users -H 'Accept: application/xml'
curl http://localhost:8080/ucurl http://localhost:8080/users -H 'Accept: application/json'

同样,也可以使用 produces 来指明返回内容 Content-Type 的设置:

1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/users",
method = RequestMethod.GET,
produces = "application/json")
public @ResponseBody List<User> allUsers() {
List<User> users = new ArrayList<User>(2);
users.add(createUser("Tom", "123"));
users.add(createUser("Jerry", "456"));
return users;
}

当客户端能接受的 Accept: application/json 与服务器能够返回的数据 Content-Type: application/xml 不匹配的时候,将会产生 406 请求错误:

1
2
3
4
5
HTTP/1.1 406 Not Acceptable
Date: Thu, 20 Jul 2017 09:11:37 GMT
Cache-Control: must-revalidate,no-cache,no-store
Content-Length: 0
Server: Jetty(9.4.6.v20170531)

另外添加依赖的时候,也要注意相同 version 统一的问题。

用户认证自动跳转到登录页

添加安全认证依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>
1
2
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}

进行用户配置:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("Tom").password("123").roles("USER").and()
.withUser("Jerry").password("456").roles("USER", "ADMIN");
}

}

当用户首次访问的时候,将会默认跳转到登录页面:


注意,当开启 Web 安全认证机制的时候,为了防止 CSRF 问题,所有表单提交的时候,都应该携带一个 _csrf 字段,否则会出现如下错误。Starting with Spring Security 3.2, CSRF protection is enabled by default. In fact, unless you take steps to work with CSRF protection or disable it, you’ll probably have trouble getting the forms in your application to submit successfully:

通过在表单中引入如下语句即可解决问题:

1
2
3
4
5
6
7
8
9
10
<form action="/submit-params-form" method="POST">
Name:<br>
<input type="text" name="name" /><br>
Password:<br>
<input type="password" name="password" /><br>
<input type="submit" value="Submit" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>

当需要上传文件的时候,一定要确保配置了:

1
2
3
4
5
6
7
8
public class SpittrWebAppInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected void customizeRegistration(javax.servlet.ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/tmp/"));
}
}

否则还是会报 CSRF 错误问题

页面拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 不加 formLogin, 会返回 403
http.formLogin()
// .loginPage("/login")
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/home").hasRole("USER")
.regexMatchers("/.*form.*").authenticated()
.anyRequest().permitAll();
}

}

缓存

添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.4.RELEASE</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

使用 @CachePut 来缓存这个方法返回的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class HomeController {

@CachePut("userList")
@RequestMapping(value = "/users",
method = RequestMethod.GET,
produces = "application/json")
public @ResponseBody List<User> allUsers() {
List<User> users = new ArrayList<User>(2);
users.add(createUser("Tom", "123"));
users.add(createUser("Jerry", "456"));
return users;
}

}

与此同时,User 类必须能够序列化:

1
2
public class User implements Serializable {
}

访问数据库

添加依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>

配置数据源:

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

@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setUrl("jdbc:mysql://localhost:3306/crawl");
return dataSource;
}

@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}

@Bean
public UserRepository userRepository(JdbcTemplate jdbcTemplate) {
return new JdbcUserRepository(jdbcTemplate);
}

}

数据库操作实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class JdbcUserRepository implements UserRepository {

private JdbcTemplate jdbcTemplate;

public JdbcUserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public User findOne(long id) {
return jdbcTemplate.queryForObject("select id, name, password from users where id=?", new UserRowMapper(), id);
}

private static final class UserRowMapper implements RowMapper<User> {
public User mapRow(ResultSet resultSet, int i) throws SQLException {
long id = resultSet.getLong("id");
String userName = resultSet.getString("name");
String password = resultSet.getString("password");
return createUser(id, userName, password);
}
}

}

使用数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class HomeController {

@Autowired
private UserRepository userRepository;

@CachePut("userList")
@RequestMapping(value = "/users",
method = RequestMethod.GET,
produces = "application/json")
public @ResponseBody List<User> allUsers() {
List<User> users = new ArrayList<User>(4);
User user = userRepository.findOne(1);
users.add(user);
return users;
}

}

中文支持

首先 JSP 页面需要加入:

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

另外还需要加入 CharacterEncodingFilter :

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
filter.setForceEncoding(true);
http.addFilterBefore(filter, CsrfFilter.class);
}

}

上传文件

添加可选依赖:

1
2
3
4
5
6
<!-- 可以方便我们操作,不是必须的 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>

form 表单标注 enctypemultipart/form-data, This tells the browser to submit the form as multipart data instead of form data. Each field has its own part in the multipart request:

1
2
3
4
5
6
7
8
9
10
11
<form action="/post-multipart-file-upload-form" method="POST" enctype="multipart/form-data">
Name:<br />
<input type="text" name="name" /><br />
Password:<br />
<input type="password" name="password" /><br />
<input type="file" name="file" /><br />
<input type="submit" value="Submit" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>

然后使用 MultipartFile 来接受上传的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping(value = "/post-multipart-file-upload-form", method = RequestMethod.POST)
@ResponseBody
public String uploadForm(MultipartFile file, @RequestParam(name = "name") String name,
@RequestParam(name = "password") String password) {
System.out.println(String.format("name=%s, password=%s", name, password));

try {
FileUtils.writeByteArrayToFile(new File(getFileSaveFolderPath() + file.getOriginalFilename()),
file.getBytes());
} catch (IOException e) {
return e.toString();
}

return "OK";
}

上传多个文件

使用 @RequestPartjavax.servlet.http.Part 能够方便我们上传多个文件:

1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/post-multiple-file", method = RequestMethod.POST)
@ResponseBody
public String uploadMultipleFile(@RequestPart("file1") Part filePart1,
@RequestPart("file2") Part filePart2,
@RequestParam String name) {
File file = new File(getFileSaveFolderPath() + filePart1.getSubmittedFileName())
FileUtils.copyInputStreamToFile(filePart1.getInputStream(), file);
// ...
}

检查文件是否存在不能用如下代码来判断:

1
2
if (filePart1 != null) {
}

而应该判断输入流的可读字节长度:

1
2
3
4
5
6
7
8
9
private boolean hasFile(Part apkFilePart) {
try {
InputStream inputStream = apkFilePart.getInputStream();
return inputStream.available() > 0;
} catch (IOException e) {
// Ignore It
return false;
}
}

上传文件的时候遇见的异常


1
2
3
4
5
6
7
8
java.lang.IllegalStateException: Form too large: 19857233 > 200000
at org.eclipse.jetty.server.Request.extractFormParameters(Request.java:365) ~[jetty-server-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.server.Request.extractContentParameters(Request.java:303) ~[jetty-server-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.server.Request.extractParameters(Request.java:257) ~[jetty-server-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.server.Request.getParameter(Request.java:826) ~[jetty-server-9.2.15.v20160210.jar:9.2.15.v20160210]
at javax.servlet.ServletRequestWrapper.getParameter(ServletRequestWrapper.java:194) ~[javax.servlet-api-3.1.0.jar:3.1.0]
at javax.servlet.ServletRequestWrapper.getParameter(ServletRequestWrapper.java:194) ~[javax.servlet-api-3.1.0.jar:3.1.0]
at org.jasig.cas.client.util.CommonUtils.safeGetParameter(CommonUtils.java:380) ~[cas-client-core-3.4.1.jar:3.4.1]

Request.java

1
2
3
4
gretty {
jvmArgs = ['-Dorg.eclipse.jetty.server.Request.maxFormContentSize=100000000']
// ...
}

1
2
3
4
5
6
7
8
9
org.eclipse.jetty.util.Utf8Appendable$NotUtf8Exception: Not valid UTF8! byte Ef in state 2
at org.eclipse.jetty.util.Utf8Appendable.appendByte(Utf8Appendable.java:195) ~[jetty-util-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.util.Utf8Appendable.append(Utf8Appendable.java:134) ~[jetty-util-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.util.UrlEncoded.decodeUtf8To(UrlEncoded.java:595) ~[jetty-util-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.util.UrlEncoded.decodeTo(UrlEncoded.java:636) ~[jetty-util-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.server.Request.extractFormParameters(Request.java:371) ~[jetty-server-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.server.Request.extractContentParameters(Request.java:303) ~[jetty-server-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.server.Request.extractParameters(Request.java:257) ~[jetty-server-9.2.15.v20160210.jar:9.2.15.v20160210]
at org.eclipse.jetty.server.Request.getParameter(Request.java:826) ~[jetty-server-9.2.15.v20160210.jar:9.2.15.v20160210]

测试 Controller

添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.9.RELEASE</version>
<scope>test</scope>
</dependency>

执行测试,首先创建 MockMvc 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
MockMvc mockMvc;

@Before
public void setup() {
InternalResourceViewResolver resourceViewResolver = new InternalResourceViewResolver();
resourceViewResolver.setPrefix("/WEB-INF/views/");
resourceViewResolver.setSuffix(".jsp");
resourceViewResolver.setExposeContextBeansAsAttributes(true);

mockMvc = MockMvcBuilders.standaloneSetup(new HomeController())
.setViewResolvers(resourceViewResolver)
.build();
}

注意在这里,一定要显示地设置上 ViewResolver ,否则会出现如下错误:

1
MockMvc : Circular view path [view]: would dispatch back to the current handler URL [/view] again

然后模拟请求并测试返回内容:

1
2
3
4
5
6
7
8
@Test
public void testPageWithDynamicValue() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/page-with-dynamic-value"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("page-with-dynamic-value"))
.andExpect(MockMvcResultMatchers.model().attributeExists("dynamicValue"))
.andExpect(MockMvcResultMatchers.model().attribute("dynamicValue", "Dynamic Value"));
}

如何正确的测试 Controller

一般测试 Controller,都是通过 Mock Service 来测的:

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
public class PeopleControllerTest {

@InjectMocks
PeopleController controller;

@Mock
PeopleService mockPeopleService;

@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}

@Test
public void testListPeopleInGroup() {
List<Person> expectedPeople = asList(new Person());
when(mockPeopleService.listPeople("group")).thenReturn(expectedPeople);

ModelMap modelMap = new ModelMap();
String viewName = controller.listPeopleInGroup("group", modelMap);

assertEquals("peopleList", viewName);
assertThat(modelMap, hasEntry("people", (Object) expectedPeople));
}
}

使用 Spring MVC Test 框架之后:

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
public class PeopleControllerTest {

@InjectMocks
PeopleController controller;

@Mock
PeopleService mockPeopleService;

@Mock
View mockView;

MockMvc mockMvc;

@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mockMvc = standaloneSetup(controller)
.setSingleView(mockView)
.build();
}

@Test
public void testListPeopleInGroup() throws Exception {
List<Person> expectedPeople = asList(new Person());
when(mockPeopleService.listPeople("someGroup")).thenReturn(expectedPeople);

mockMvc.perform(get("/people/someGroup"))
.andExpect(status().isOk())
.andExpect(model().attribute("people", expectedPeople))
.andExpect(view().name("peopleList"));
}
}

Spring + MyBatis + Transaction Management

MyBatis SqlSession provides you with 特定方法 to handle transactions 通过编程. But when using MyBatis-Spring your beans will be injected with a Spring managed SqlSession or a Spring managed mapper. That means that Spring will always handle your transactions.

You 不能调用 SqlSession.commit(), SqlSession.rollback() or SqlSession.close() over a Spring managed SqlSession. If you try to do so, a UnsupportedOperationException exception will be thrown. Note these methods are not exposed in injected mapper classes.

Regardless of your JDBC connection’s autocommit setting, any execution of a SqlSession data method or any call to a mapper method outside a Spring transaction will be automatically committed.

If you want to control your transactions programmatically please refer to chapter 10.6 of the Spring reference manual. This code shows how to handle a transaction manually using the PlatformTransactionManager described in section 10.6.2.

1
2
3
4
5
6
7
8
9
10
11
12
13
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = txManager.getTransaction(def);
try {
userMapper.insertUser(user);
}
catch (MyException ex) {
txManager.rollback(status);
throw ex;
}

txManager.commit(status);

  • Spring 默认事务隔离级别: Isolation.DEFAULT:

MySQL 默认事务隔离级别: InnoDB offers all four transaction isolation levels described by the SQL:1992 standard: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE. The default isolation level for InnoDB is REPEATABLE READ 可重复读.

  • Spring 默认事务传播机制: Propagation.REQUIRED
1
2
3
4
5
6
7
8
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
}

性能: 脏读 > 读写提交 > 可重复读 > 序列化

四种事务解释如下:

(1) READ_UNCOMMITTED:

READ_UNCOMMITTED isolation level states that a transaction may read data that is still uncommitted by other transactions 一个事务可能会读到一个其它事务未提交的脏数据

(2) READ_COMMITTED 读写提交:

READ_COMMITTED isolation level states that a transaction can’t read data that is not yet committed by other transactions 一个事务无法阅读其它事务还未提交的数据.

读写提交产生的问题:

(3) REPEATABLE READ:

可重复读是针对于同一条记录而言的,对于不同的记录会发生下面的场景:

幻读是针对删除和插入的。

(4) SERIALIZABLE:

所有的操作都会按照顺序执行,不会出现脏读、不可重读和幻读的情况:

任务调度 - Quartz

web.xml

context-param

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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">

<!-- 值可以被所有的 Servlets 读取 -->
<context-param>
<!-- 根上下文的位置 -->
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>

</web-app>

listener

监听 web 容器中的事件

1
2
3
4
5
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
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
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

/**
* Bootstrap listener to start up and shut down Spring's root.
*/
public class ContextLoaderListener extends ContextLoader
implements ServletContextListener {

/**
* 初始化根 web 应用的上下文
*/
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}

/**
* 关闭根 web 应用的上下文
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}

}

servlet

servlet 是一个 Java 编写的程序,其生命周期分为: 初始化、运行和销毁

(1) 初始化
  • servlet 容器加载 servlet 类,把 servlet 类的 .class 文件读取到内存中
  • servlet 容器创建一个 ServletConfig 对象。ServletConfig 对象包含了 servlet 的初始化配置信息。
  • servlet 容器创建一个 servlet 对象
  • servlet 容器调用 servlet 对象的 init 方法进行初始化
(2) 运行

servlet 容器接收到一个请求时,servlet 容器会针对这个请求创建 ServletRequestServletResponse 对象,然后调用 service 方法。

(3) 销毁
  • servlet 容器先调用 servlet 对象的 destroy 方法,然后再销毁 servlet 对象,以及 ServletConfig 对象。

filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

自定义 filter:

1
2
3
4
5
6
7
8
9
<filter>
<filter-name>userinfofilter</filter-name>
<filter-class>plm.common.filters.UserFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>userinfofilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Tomcat 读取 web.xml 各个节点的顺序

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
public class StandardContext extends ContainerBase
implements Context, NotificationEmitter {

@Override
protected synchronized void startInternal() throws LifecycleException {
// Merge the context initialization parameters specified in the application
// deployment descriptor with the application parameters described in the
// server configuration.
mergeParameters();

// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
ok = false;
}
}

// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}

// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}

}

private void mergeParameters() {
ServletContext sc = getServletContext();
for (Map.Entry<String,String> entry : mergedParams.entrySet()) {
sc.setInitParameter(entry.getKey(), entry.getValue());
}
}

}

Tomcat 解析 web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ContextConfig implements LifecycleListener {

/**
* Scan the web.xml files that apply to the web application and merge them
* using the rules defined in the spec.
*/
protected void webConfig() {
WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
context.getXmlValidation(), context.getXmlBlockExternal());

if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
ok = false;
}
}

}

使用 org.xml.sax 来解析,SAX 基于事件的解析,解析器在一次读取 XML 文件中根据读取的数据产生相应的事件,由应用程序实现相应的事件处理逻辑,即它是一种“推”的解析方式;这种解析方法速度快、占用内存少,但是它需要应用程序自己处理解析器的状态,实现起来会比较麻烦。

JAVA 解析 XML 通常有两种方式, org.w3c.domSAX。DOM 虽然是 W3C 的标准,提供了标准的解析方式,但它的解析效率一直不尽如人意,因为使用 DOM 解析 XML 时,解析器读入整个文档并构建一个驻留内存的树结构(节点树),然后您的代码才可以使用 DOM 的标准接口来操作这个树结构。但大部分情况下我们只对文档的部分内容感兴趣,根本就不用先解析整个文档,并且从节点树的根节点来索引一些我们需要的数据也是非常耗时的。

SAX是一种XML解析的替代方法。相比于文档对象模型DOM,SAX 是读取和操作 XML 数据的更快速、更轻量的方法。SAX 允许您在读取文档时处理它,从而不必等待整个文档被存储之后才采取操作。它不涉及 DOM 所必需的开销和概念跳跃。 SAX API是一个基于事件的API ,适用于处理数据流,即随着数据的流动而依次处理数据。SAX API 在其解析您的文档时发生一定事件的时候会通知您。在您对其响应时,您不作保存的数据将会被抛弃。

Spring 如何映射 RequestMapping

重点关注 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping 这个类。

异常处理

No Converter

1
Caused by: java.lang.IllegalArgumentException: No converter found for return value of type: class com.zk.ResultBean

The problem was that one of the nested objects in Foo didn’t have any getter/setter

WEB-INF

部署 Java EE 项目必须遵守规范:

  • The servlet container (e.g Tomcat)
  • Servlet 规范

  • WEB-INF 目录不是 public 目录,浏览器不能直接访问
  • 使用 ServletContext 类上面的 getResourcegetResourceAsStream 方法能够直接访问 WEB-INF 目录
  • 确保开发项目的目录结构和最后打成 WAR 包的目录结构不同,开发项目的目录结构,经过 build 过程,变为 WAR 包的目录结构
  • src/main/javasrc/main/resources 目录编译后会被放进 WEB-INF/classes 目录下面
  • Servlet 3.0 之后, 放置在 WEB-INF/lib/xxx.jar/META-INF/resources 目录下的也可以能够被浏览器直接访问了:

以上文件可以用如下 URL 分别访问:

  • http://host:port/webcontext/scripts.js
  • http://host:port/webcontext/styles.css
  • http://host:port/webcontext/welcome.png

推荐文章