Java - SpringMVC 框架详解(二)

3. SpringMVC Helloworld

3.5. 使用Thymeleaf显示页面

当项目中需要使用Thymeleaf模版时,就需要添加thymeleaf的依赖,并且,由于是应用在SpringMVC项目中,还需要添加thymeleaf-spring4的依赖:

<dependency>
	<groupId>org.thymeleaf</groupId>
	<artifactId>thymeleaf</artifactId>
	<version>3.0.11.RELEASE</version>
</dependency>

<dependency>
	<groupId>org.thymeleaf</groupId>
	<artifactId>thymeleaf-spring4</artifactId>
	<version>3.0.11.RELEASE</version>
</dependency>

以上依赖的备选版本还有:3.0.2,3.0.9。

由于的是Thymeleaf模版技术来显示页面,所以,还应该创建对应的HTML页面,这个页面将是客户端发出请求后,最后能够显示在浏览器中的页面!

可以在src/main/resources下创建web文件夹(该文件夹的名称是自定义的),然后在该文件夹下创建welcome.html文件:

请添加图片描述

创建好HTML文件好,可以自行设计页面的内容。

然后,需要配置ViewResolver(视图解析器),在SpringMVC中,ViewResolver是一个接口,可以使用的实现类的种类较多,对应使用不同的技术呈现页面,甚至使用不同的页面,在Java EE的默认体系下,可以使用InternalResourceViewResolver呈现JSP页面,此次使用的是Thymeleaf模版技术,所以,应该使用ThymeleafViewResolver,需要在spring.xml中进行配置,关于这个类,主要配置thymeleafEnginecharacterEncoding,而thymeleafEngine的值应该是SpringTemplateEngine的对象,所以,还需要在spring.xml中配置这个类,但是,在模版引擎中,还需要配置模版解析器TemplateResolver,此次可用的TemplateResolverClassLoaderTemplateResolver,所以,继续添加配置!完整的配置代码例如:

<!-- 配置模版解析器 -->
<bean id="templateResolver"
	class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver">
	<!-- 前缀 -->
	<property name="prefix" value="/web/" />
	<!-- 后缀 -->
	<property name="suffix" value=".html" />
	<!-- 字符编码 -->
	<property name="characterEncoding" value="UTF-8" />
	<!-- 模版模式 -->
	<property name="templateMode" value="HTML" />
	<!-- 是否缓存 -->
	<property name="cacheable" value="false" />
</bean>
	
<!-- 配置模版引擎 -->
<bean id="templateEngine" class="org.thymeleaf.spring4.SpringTemplateEngine">
	<!-- 模版解析器 -->
	<property name="templateResolver" ref="templateResolver" />
</bean>
	
<!-- 配置视图解析器:ThymeleafViewResolver -->
<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
	<!-- 模版引擎 -->
	<property name="templateEngine" ref="templateEngine" />
	<!-- 字符编码 -->
	<property name="characterEncoding" value="UTF-8" />
</bean>

完成后,还需要修改HelloController中处理请求的方法的返回值为welcome,要求根据“前缀 + 方法返回值 + 后缀”能得到页面文件的位置!所以:

@RequestMapping("hello.do")
public String showHello() {
    
    
	System.out.println("HelloController.showHello()");
	return "welcome";
}

如果处理请求的方法返回null,等同于返回映射的地址中小数点左侧的名称!但是,并不推荐这样处理!

扫描二维码关注公众号,回复: 14860566 查看本文章

全部完成后,启动项目,打开浏览器,输入网址即可访问!

另外,在SpringMVC + Thymeleaf项目中,可使用的模版解析器有ClassLoaderTemplateResolverServletContextTemplateResolver,使用前者,应该将HTML模版文件放在src/main/resources下,使用后者,应该将HTML模版文件放在webapp下。

4. 接收客户端提交的请求参数

4.1. 准备案例

目标:在浏览器访问http://localhost:8080/项目名称/reg.do时,可以打开注册页面,页面中至少包括用户名、密码、年龄、手机号码、电子邮箱这5项数据的输入框和1个提交按钮!

首先,创建Maven Project项目,创建过程中勾选Create a simple projectGroup Idcn.teduArtifact IdSpringMVC02Packaging选择war

当项目创建出来后,需要:

  • 生成web.xml
  • 从前序项目中复制依赖的代码到当前项目的pom.xml中;
  • 从前序项目中复制spring.xml到当前项目中;
  • 对项目点右键,在属性中勾选Tomcat;
  • 打开前序项目的web.xml,将DispatcherServlet的配置复制到当前项目的web.xml中。

然后创建cn.tedu.spring.UserController控制器类,需要注意:该类必须放在组件扫描的包下,所以,应该检查spring.xml中的组件扫描的配置。

在控制器类中添加处理请求的方法:

@Controller
public class UserController {
    
    

	@RequestMapping("reg.do")
	public String showReg() {
    
    
		System.out.println("UserController.showReg()");
		return "reg";
	}
	
}

结合spring.xml中的配置,应该在src/main/resources下创建web文件夹,并在该文件夹下创建页面。

创建完成后,在浏览器中打开http://localhost:8080/SpringMVC02/reg.do即可看到所创建的页面。

如果需要获取客户端提交的注册请求,应该先在控制器类中再添加一个接收这个请求的方法:

@RequestMapping("handle_reg.do")
public String handleReg() {
    
    
	System.out.println("UserController.handleReg()");
	return null;
}

然后,在页面中,将<form>action属性设置为handle_reg.do即可,当在页面点击提交按钮时,就会将请求提交到以上控制器方法!

4.2. 通过HttpServletRequest对象获取请求参数【不推荐】

可以在处理请求的方法的参数列表中添加HttpServletRequest类型的参数,在方法的处理过程中,调用该参数对象的String getParameter(String name)方法,即可获取客户端提交的请求参数:

@RequestMapping("handle_reg.do")
public String handleReg(HttpServletRequest request) {
    
    
	System.out.println("UserController.handleReg()");
	
	String username = request.getParameter("username");
	String password = request.getParameter("password");
	String age = request.getParameter("age");
	String phone = request.getParameter("phone");
	String email = request.getParameter("email");
	System.out.println("username=" + username);
	System.out.println("password=" + password);
	System.out.println("age=" + age);
	System.out.println("phone=" + phone);
	System.out.println("email=" + email);
	
	return null;
}

这种存在几个明显的问题:

  1. 获取参数的过程比较麻烦;

  2. 需要自行处理非String类型的数据的类型转换;

  3. 不便于执行单元测试。

4.3. 将请求参数声明为处理请求的方法的参数【推荐】

在处理请求的方法的参数列表中,一一添加客户端提交的请求参数,并保持使用相同的名称即可:

@RequestMapping("handle_reg.do")
public String handleReg(String username, String password, 
		Integer age, String phone, String email) {
    
    
	System.out.println("UserController.handleReg()");
	
	System.out.println("username=" + username);
	System.out.println("password=" + password);
	System.out.println("age=" + (age + 1));
	System.out.println("phone=" + phone);
	System.out.println("email=" + email);
	
	return null;
}

默认情况下,处理请求的方法的参数名称,必须与客户端提交的参数名称保持一致,否则,处理请求的方法的参数值将是null

在处理请求的方法中,参数列表中的各参数是不区分先后顺序的!

如果客户端提交了多个同名的请求参数,在控制器中处理请求的方法的参数列表中,可以使用数组格式来接收这些请求参数,例如客户端添加了“专业技能”:

<td>专业技能</td>
<td>
	<input type="checkbox" name="skill" value="Java OOP" />Java OOP
	<input type="checkbox" name="skill" value="Java SE" />Java SE
	<input type="checkbox" name="skill" value="MySQL" />MySQL
	<input type="checkbox" name="skill" value="JDBC" />JDBC
	<input type="checkbox" name="skill" value="Java EE" />Java EE
</td>

则服务器端的控制器中处理请求的方法:

@RequestMapping("handle_reg.do")
public String handleReg(String username, String password, 
		Integer age, String phone, String gender, String email, String[] skill) {
    
    
	System.out.println("UserController.handleReg()");
	
	System.out.println("username=" + username);
	System.out.println("password=" + password);
	System.out.println("gender=" + gender);
	System.out.println("age=" + (age + 1));
	System.out.println("phone=" + phone);
	System.out.println("email=" + email);
	System.out.println("skill=" + skill);
	System.out.println("skill.length=" + skill.length);
	for (int i = 0; i < skill.length; i++) {
    
    
		System.out.println("skill[" + i + "]=" + skill[i]);
	}
	
	return null;
}

使用这种方式获取请求参数非常简单,但是,不太适用于请求参数较多的应用场景!

4.4. 使用封装的类型作用处理请求的方法的参数【推荐】

当请求参数的数量较多时,可以将这些数据都封装在一个自定义的数据类型中,例如:

public class User {
    
    

	private String username;
	private String password;
	private Integer age;
	private String phone;
	private String gender;
	private String email;
	private String[] skill;

	// 规范的SET/GET方法

}

然后,使用User作为彼得请求的方法的参数即可:

@RequestMapping("handle_reg.do")
public String handleReg(User user) {
    
    
	System.out.println("UserController.handleReg()");
	
	System.out.println(user);
	
	return null;
}

使用这种做法时,必须保证请求参数的名称与封装类(例如以上User类)的属性的名称是一致的

由于SpringMVC框架在处理时,会自动调用封装类(例如以上User类)相关属性的SET/GET方法,所以,必须保证相关属性有规范的SET/GET方法!

4.5. 小结

以上介绍了3种获取请求参数的做法,其中,第1种使用HttpServletRequest的做法是不推荐的!另外,还有第2种在参数列表中穷举请求参数,和第3种封装请求参数的做法都是推荐使用的!

当请求参数的数量较少(例如少于4个),并且参数的数量相对固定,或各数据都不可以归属于同一种类型时,应该优先使用第2种做法;

当请求参数的数量较多(例如超过4个),并且可以归属于同一种类型时,应该优先使用第3种做法,或,参数的数量可能发生变化,也应该优先使用第3种做法;

另外,以上第2种和第3种做法可以组合一起使用

5. 转发数据

5.1. 准备工作

在处理登录的过程中,模拟判断登录,暂不考虑登录成功的处理方式,当登录失败时,由专门的错误提示页面来显示错误信息:

// 假设root/1234是正确的用户名/密码
if ("root".equals(username)) {
    
    
	// 用户名正确,需要判断密码
	if ("1234".equals(password)) {
    
    
		// 密码也正确,则登录成功
		System.out.println("登录成功!");
	} else {
    
    
		// 密码错误
		System.out.println("登录失败,密码错误!");
		return "error";
	}
} else {
    
    
	// 用户名错误
	System.out.println("登录失败,用户名不存在!");
	return "error";
}

具体的错误描述应该是控制器转发给模版页面。

5.2. 使用HttpServletRequest封装需要转发的数据【不推荐】

在处理请求的方法的参数列表中添加HttpServletRequest参数,然后,当需要封装转发数据时,调用该参数对象的setAttribute(String name, Object value)方法进行封装即可:

// 假设root/1234是正确的用户名/密码
if ("root".equals(username)) {
    
    
	// 用户名正确,需要判断密码
	if ("1234".equals(password)) {
    
    
		// 密码也正确,则登录成功
		System.out.println("登录成功!");
	} else {
    
    
		// 密码错误
		System.out.println("登录失败,密码错误!");
		request.setAttribute("errorMessage", "登录失败,密码错误!");
		return "error";
	}
} else {
    
    
	// 用户名错误
	System.out.println("登录失败,用户名不存在!");
	request.setAttribute("errorMessage", "登录失败,用户名不存在!");
	return "error";
}

后续,在页面中需要显示封装的转发数据时,使用Thymeleaf表达式进行处理即可:

<h3 th:text="${errorMessage}"></h3>

这种做法并不推荐,因为它仍存在“不便于执行单元测试”的问题!

5.3. 使用ModelMap封装需要转发的数据【推荐】

关于使用ModelMap封装需要转发的数据,做法与使用HttpServletRequest几乎相同:

@RequestMapping("handle_login.do")
public String handleLogin(String username, String password,
	ModelMap modelMap) {
    
    
	System.out.println("UserController.handleLogin()");
	
	System.out.println("username=" + username);
	System.out.println("password=" + password);
	
	// 假设root/1234是正确的用户名/密码
	if ("root".equals(username)) {
    
    
		// 用户名正确,需要判断密码
		if ("1234".equals(password)) {
    
    
			// 密码也正确,则登录成功
			System.out.println("登录成功!");
		} else {
    
    
			// 密码错误
			System.out.println("登录失败,密码错误!");
			modelMap.addAttribute("errorMessage", "[ModelMap] 登录失败,密码错误!");
			return "error";
		}
	} else {
    
    
		// 用户名错误
		System.out.println("登录失败,用户名不存在!");
		modelMap.addAttribute("errorMessage", "[ModelMap] 登录失败,用户名不存在!");
		return "error";
	}
	
	return null;
}

尽管使用方式几乎是一样的,但是,ModelMap这种类型更加轻量级,并且易于执行单元测试!

5.4. 使用ModelAndView作为处理请求方法的返回值【不推荐】

将处理请求的方法的返回值类型声明为ModelAndView,可以调用ModelAndView(String viewName, Map<String, ?> model)构造方法创建对象,该构造方法的第1个参数String viewName表示视图名,第2个参数Map<String, ?> model表示转发的数据:

@RequestMapping("handle_login.do")
public ModelAndView handleLogin(String username, String password) {
    
    
	System.out.println("UserController.handleLogin()");
	
	System.out.println("username=" + username);
	System.out.println("password=" + password);
	
	// 假设root/1234是正确的用户名/密码
	if ("root".equals(username)) {
    
    
		// 用户名正确,需要判断密码
		if ("1234".equals(password)) {
    
    
			// 密码也正确,则登录成功
			System.out.println("登录成功!");
		} else {
    
    
			// 密码错误
			System.out.println("登录失败,密码错误!");
			Map<String, Object> model = new HashMap<String, Object>();
			model.put("errorMessage", "[ModelAndView] 登录失败,密码错误!");
			ModelAndView mav = new ModelAndView("error", model);
			return mav;
		}
	} else {
    
    
		// 用户名错误
		System.out.println("登录失败,用户名不存在!");
		Map<String, Object> model = new HashMap<String, Object>();
		model.put("errorMessage", "[ModelAndView] 登录失败,用户名不存在!");
		ModelAndView mav = new ModelAndView("error", model);
		return mav;
	}
	
	return null;
}

这种做法相比此前使用ModelMap更加麻烦,所以,一般不推荐使用!

当然,使用ModelMap时,SpringMVC的运行原理最终还是会处理为ModelAndView类型的数据,只不过这部分不需要开发者来完成!

6. 重定向

当处理请求的方法的返回值是String类型时,默认情况下,表示的意思是转发,该返回值会与配置的前缀和后缀进行拼接,从而确定最终负责显示的视图组件!如果返回值使用redirect:作为前缀,加上目标路径,就可以实现重定向!例如:

return "redirect:login.do";

注意:当使用重定向时,在redirect:右侧的是目标路径,该路径会直接响应给客户端,并不会参与前缀后缀的拼接,后续客户端会根据这个路径发出第2次请求!

转发也可以使用forward:作为前缀,只不过,默认就表示转发,所以,不需要添加这个前缀。

----------------------------------------------

附1:关于封装

封装就是先“装”了再“封”的过程!

在许多应用场景中,可能需要使用或传递或克隆多项数据,为了便于管理,可以将这些数据“装”在某一个类型中,例如:

public class User {
    
    
	public String username;
	public String password;
	public Integer age;
}

后续,关于这个类的使用可以是:

User user = new User();
user.username = "Mike";
user.password = "1223";
user.age = 9527;

以上代码就出现了“合法却不合理”的问题!

为了避免类的属性被随便恶意访问,可以限制属性的访问:

public class User {
    
    
	public String username;
	public String password;
	private Integer age;
}

限制了访问权限,其实,就是“封”的过程!

但是,“封”的目的并不是“不允许访问”,而是“不允许随便访问”,为了保证这个属性还是可以被访问的,可以为这个属性添加公有的SET方法:

public class User {
    
    
	public String username;
	public String password;
	private Integer age;

	public void setAge(Integer age) {
    
    
		this.age = age;
	}
}

后续,在类的外部,就可以通过user.setAge(9527);这类语法来访问Integer age属性!

由于使用了方法,则可以在方法中编写相关规则:

public void setAge(Integer age) {
    
    
	if (age >= 0 && age <= 150) {
    
    
		this.age = age;
	}
}

最后,为了保证外部还可以获取到该属性的值,通常,还会定义公有的GET方法:

public Integer getAge() {
    
    
	return age;
}

另外,除了类的属性可以封装以外,类本身也是可以封装的,例如工厂模式就是一种对类进行封装的表现。

附2:转发与重定向

请添加图片描述

转发与重定向都是控制器可以使得客户端浏览器显示某个页面的做法!

在转发的处理过程中,客户端只向服务器发出过1次请求;在重定向的处理过程中,客户端发出第1次请求后,服务器端会响应302及重定向的目标位置,客户端的浏览器接收到302响应码后,会根据目标位置发出第2次请求!

在转发的过程中,是涉及2个服务器内部的组件,所以,2个组件(控制器与页面)之间是可以传递任何类型的数据的;在重定向的过程中,由于是2次请求,并且Http协议是无状态协议,2次处理过程中的数据并不能直接共享使用!

由于转发是1次请求,所以,在客户端的浏览器的地址栏中,显示的就是最初发出请求的路径;由于重定向是2次请求,所以,最终在客户端的浏览器的地址栏中,显示的是最后一次请求的路径!

如果这篇文章有帮助到您,请简单给个赞吧,谢谢~

猜你喜欢

转载自blog.csdn.net/Kevinblant/article/details/120858018
今日推荐