天天看点

我的Java Web之路 - Spring MVC和Spring IoC初步使用

本系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考,希望能帮助更多码农和想成为码农的人。

本文转发自头条号【普通的码农】的文章,大家可以关注一下,直接在今日头条的移动端APP中阅读。因为平台不同,会出现有些格式、图片、链接无效方面的问题,我尽量保持一致。

文章目录

    • 介绍
    • 分层思维和单一职责思维
    • 思路
    • 配置Spring MVC和Spring IoC
    • 展示层
    • 模型层
    • 控制器层
    • 验证
    • 中文乱码问题再现
    • 总结

介绍

我们的租房网应用从刚开始的使用Servlet建立,到使用Filter来消除重复,再到使用JSP将展示层初步分离,再到使用JSTL和EL表达式彻底分离展示层,现在看起来已经很不错了。

不过,还存在以下问题:

• 控制器层和模型层尚未分离;

• 如果要使用JSP以外的展示层技术,则还要做很多工作;

• 等等

而要解决这些问题,MVC框架和IoC框架是一个很好的选择,Spring MVC和Spring IoC就是其中一个很流行的组合。

我们前面已经介绍过Spring MVC的核心原理、使用XML配置和整合IoC、使用Java配置和整合IoC、整合基于注解和Java的IoC、基于注解的控制器。

也介绍过Spring IoC的核心原理、基于XML生产和装配Bean、使用注解自动装配Bean、使用注解生产Bean、使用Java生产和装配Bean

所以,我们继续使用Spring MVC和Spring IoC来改造租房网应用吧。

分层思维和单一职责思维

分层思维和单一职责思维紧密联系、密不可分。

分层思维,也可以叫做层级、级别等等;

单一职责思维,就是只干一件事;

它们都可谓是无处不在。

自然界中,蚂蚁、蜜蜂这两个团队都是依据分层和单一职责来划分的。

国家行政机关有省部厅司局等,组织机构也有类似的分层,这些都是管理范畴的。

公路网、铁路网、电信网络等都有主干、次级主干、分支等,这也是一种分层。

做事情的规划也可以分层,百年规划、五十年规划、三十年规划、十年规划、五年规划。

在我们IT行业中,操作系统中也有硬件层、内核层、应用层等;计算机网络中有物理层、数据链路层、网络层、传输层、会话层、表示层、应用层的OSI七层模型;而应用程序开发中,又具有更细的分层,比如MVC,甚至每一个类/接口、每一个方法/函数都可以视之为分层和单一职责的体现。

分层最大的好处是什么?就是每一层只关心和解决一件事,这实际上又是单一职责思维、高内聚的体现。而层与层之间的通信只通过规范好的接口来实现,这就是低耦合的体现。

我们依据分层思维(具体到这就是MVC了),将JSP页面用作展示层,即只是取数据进行展示,而不关心数据是如何取得的。

而数据的查找、计算和存储是由模型层来解决。

控制器层拦截请求、分派请求、串联模型层和展示层进行请求的处理(转换、绑定、校验、调用模型层、转发到展示层或其他组件)、生成响应并发送,我们采用Servlet来实现。具体到Spring MVC,又分为三个层级(DispatcherServlet、Controller、Handler)。我们把对一个请求的处理叫做一个action,所以规定提交给控制器层的请求必须以 .action 为后缀。

再加上JSP页面是以 .jsp 为后缀的,它们都可以称之为资源,只不过JSP页面是数据的展示,控制器层是数据的处理。即请求某个资源,实际上是指请求某些数据进行展示,或者请求某些数据进行处理。

当然,处理结果最后还是要进行展示,所以,最终返回给浏览器端的都是某种数据的某种展示。

思路

展示层往往是从最终用户(可能是普通用户,也可能是开发者用户哦)使用的角度去分析,我们这里主要是JSP页面,仍然是登录页面login.html、登录后会展示用户感兴趣的房源列表页面houses.jsp、可以查看某个房源详情的页面house-details.jsp、可以修改某个房源信息的编辑页面house-form.jsp。

这几个JSP页面因为之前已经改造成了纯粹展示,所以代码基本不用改变。不过,这几个JSP页面内部的链接需要修改为 .action 为后缀的,因为每个链接都需要请求服务端执行查找或处理某些数据的动作啊。

我们再来看看控制器层如何来设计。

Spring MVC的核心是DispatcherServlet,首先要在部署描述符web.xml中配置它。当然,必须配置它拦截所有 .action 为后缀的请求(上面设计的:))。而它又依赖于Spring IoC来配置和管理各种组件,比如控制器、视图解析器等等,所以要提供Spring IoC的配置元数据(基于XML、注解、Java均可)。

那么进一步看看需要几个Controller和Handler。因为我们的租房网应用现在还比较简单,所以把所有动作都放在一个Controller中就行了。

因为请求登录页面仍然是不需要任何数据,所以不需要为它设计任何动作,使用静态页面就可以了。但是,提交登录请求的时候需要服务端验证用户名和密码,这需要一个动作,就叫login.action吧。

登录之后,就需要服务端找到我感兴趣的房源啊,这需要一个动作,就叫houses.action吧。

然后,我想看某个房源的详细信息,也需要服务端去查找那个房源的详细信息(为什么不在找房源列表的同时把每一个房源的详细信息就找到呢?大家也可以想一想),这也需要一个动作,就叫house-details.action吧。

最后,我想编辑某个房源的详细信息,首先需要服务端把该房源的详细信息找到,并以表单的形式展示出来,这需要一个动作,就叫house-form.action吧;而修改完表单中该房源的详细信息之后,需要提交给服务端保存起来,这也需要一个动作,就叫house-form.action吧。哎,等等,怎么这两个动作都叫house-form.action啊?!那该怎么映射到Spring MVC中的Handler呢?这就需要将这两个动作在HTTP层面使用HTTP方法来区分,前者使用GET方法将house-form.action映射到一个Handler;后者使用POST方法映射到另一个Handler。本质上还是两个动作。

租房网目前只用到了模拟的房源数据,连最基本的用户数据都还没有,其实是不太妥当的。不过,这用来做演示已经足够了,何况我们可以慢慢的添加其他数据,一步一步来,这就是演化/迭代思维。

尽管如此,我们把模拟的房源数据也抽象出来,作为我们的模型层(按照目前流行的分层技术,又可以分为服务层和数据访问层,但我们暂时还没有必要这么分)。

根据上面分析,模型层中最主要的服务就是查找某个用户感兴趣的房源列表、根据房源ID查找该房源的详细信息。

下面,我们就一步一步来使用Spring MVC和Spring IoC来改造我们的租房网应用。

配置Spring MVC和Spring IoC

首先,我们要在我们的租房网应用中配置Spring MVC和Spring IoC。

配置的第一步,是把它们的JAR包先引入到工程结构中,可以参考这篇文章(实际上就是直接拷贝)。

我的Java Web之路 - Spring MVC和Spring IoC初步使用

第二步,是要配置Spring MVC的DispatcherServlet,可以使用XML(参考这篇文章),也可以使用Java(参考这篇文章),我这里选择的是使用XML的方式,web.xml的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	id="WebApp_ID" version="3.0">
	<display-name>house-renter</display-name>
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
		<welcome-file>index.htm</welcome-file>
		<welcome-file>index.jsp</welcome-file>
		<welcome-file>default.html</welcome-file>
		<welcome-file>default.htm</welcome-file>
		<welcome-file>default.jsp</welcome-file>
	</welcome-file-list>
	<servlet>
		<servlet-name>dispatcher</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/dispatcher.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>dispatcher</servlet-name>
		<url-pattern>*.action</url-pattern>
	</servlet-mapping>
</web-app>
           

需要关注的是:

• DispatcherServlet的名称可以随意取,不过最好取个好听的名字;

• 整合Spring IoC,使用的也是基于XML的方式;

• DispatcherServlet拦截的请求是:

*.action

,可千万不能写成:

/*.action

,否则Tomcat会启动不了。具体的映射规则可以参考Servlet规范(比如,servlet-4_0_FINAL.pdf)中的“Mapping Requests to Servlets”这一部分。

第三步是整合Spring IoC,使用的也是基于XML的方式。根据DispatcherServlet的配置,创建dispatcher.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		https://www.springframework.org/schema/beans/spring-beans.xsd 
		http://www.springframework.org/schema/context
		https://www.springframework.org/schema/context/spring-context.xsd">
	
	<context:component-scan base-package="houserenter"/>
</beans>
           

对于Spring IoC的配置,我开启了组件扫描,这样我们就能够使用Spring MVC的基于注解的控制器了。当然,我把所有组件都放在houserenter这个包和它的子包当中。

展示层

展示层的几个HTML、JSP页面的代码只需将链接和表单的URL改为 .action 后缀即可。

• login.html中表单的action属性值改为:login.action;

• include.jsp不用修改;

• houses.jsp中链接的href属性值改为:house-details.action(后面的参数仍然不变,下同);

• house-details.jsp中有两个链接,编辑链接的href属性值改为:house-form.action;回到列表链接的href属性值改为:houses.action;

• house-form.jsp中表单的action属性值改为:house-form.action,同时,method属性值必须是:post,以免跟上面的编辑链接冲突!

改为 .action 后缀的意义对开发人员来说还是挺清晰的,就是要请求服务端执行某个动作啊,当然,这种 .action 后缀的请求都将由DispatcherServlet拦截并分派给各个Controller中的各个Handler。

每个JSP页面中的数据(即绑定到该页面的数据对象的名称)仍然沿用原来的,比如房源列表页面houses.jsp使用mockHouses(它是一个House对象的List);房源详情页面和房源编辑页面使用target(它是一个House对象)。

模型层

因为控制器层依赖于模型层,所以我们还是先来实现模型层吧。

根据前面分析,我们只需要实现一个房源服务即可,就叫HouseService吧,它有两个方法,一个是查找用户感兴趣的房源列表,就叫findHousesInterested();一个是根据房源ID查找房源详情的方法,就叫findHouseById()吧。

因为HouseService将为持续到来的请求提供服务,所以应该在租房网应用运行期间初始化一个实例之后常驻内存,这样才能保证请求更加快速地得到处理。我们不就可以使用Spring IoC的@Service注解自动生成Bean并交给Spring IoC容器来管理了吗?!

它使用模拟的房源数据,当然在该组件初始化的时候装载它们即可。

当然,它仍然使用的是原来entity包中的House类。实际上,实体类也应该属于模型层。而控制器层应该将传输对象转换为实体对象,但我这里都是使用的实体对象,因为它们是一致的。

同时,为了体现分层思维,需要另外建立一个service包,将HouseService类放入此包中。

package houserenter.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import houserenter.entity.House;
@Service
public class HouseService {
	private List<House> mockHouses;
	public HouseService() {
		mockHouses = new ArrayList<House>();
		mockHouses.add(new House("1", "金科嘉苑3-2-1201", "详细信息"));
		mockHouses.add(new House("2", "万科橙9-1-501", "详细信息"));
	}
	public List<House> findHousesInterested(String userName) {
		// 这里查找该用户感兴趣的房源,省略,改为用模拟数据
		return mockHouses;
	}
	public House findHouseById(String houseId) {
		for (House house : mockHouses) {
			if (houseId.equals(house.getId())) {
				return house;
			}
		}
		return null;
	}
}
           

控制器层

根据前面的思路,我把所有动作都放在了一个控制器中,就叫HouseRenterController吧。当然,运行时需要生成它的Bean,我们仍然可以使用Spring IoC。因为我们已经开启了组件扫描,所以只要为该类加上注解@Controller即可。

然后,它依赖于模型层的HouseService,而HouseService也使用Spring IoC来自动生成Bean,所以可以使用@Autowired注解自动装配它们。

然后,就是各个Handler的声明,可以参考这篇文章。Handler的声明采用@RequestMapping及其衍生注解如@GetMapping和@PostMapping等。

各个Handler与动作是一一对应的:

• 处理登录请求:@PostMapping("/login.action")

• 获取房源列表:@GetMapping("/houses.action")

• 获取房源详情:@GetMapping("/house-details.action")

• 获取房源编辑表单:@GetMapping("/house-form.action")

• 处理房源更新请求:@PostMapping("/house-form.action")

虽然,获取房源编辑表单和处理房源更新请求的URL是一样的,但是可以通过HTTP方法来匹配这两个不同的请求。

而各个Handler的实现,也可以参考这篇文章。我主要是使用ModelAndView这个类来作为Handler的返回值;而请求中携带的参数直接使用字符串类型,所以可以省略@RequestParam注解,只要保证参数名与请求中的参数名一致即可。

ModelAndView可以绑定我们希望视图层展示的数据对象,也是保证数据的名字与视图中(这里是JSP页面)中使用的名字一致即可。它使用的是addObject()等方法。

ModelAndView可以设置希望将请求转发到哪一个视图(这里是JSP页面)或动作,或者重定向到哪一个视图(这里是JSP页面)或动作。它使用的是setViewName()等方法,重定向的视图名需要带一个前缀:“redirect:”。

至于各个Handler具体的业务处理规则,还是相当简单的。

package houserenter.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import houserenter.entity.House;
import houserenter.service.HouseService;
@Controller
public class HouseRenterController {
	
	@Autowired
	private HouseService houseService;
	@GetMapping("/test.action")
	@ResponseBody
	public String test() {
		return "hello";
	}
	@PostMapping("/login.action")
	public ModelAndView postLogin(String userName, String password) {
		//这里需要验证用户是否已经注册,省略
		System.out.println("userName: " + userName + ", password: " + password);
		ModelAndView mv = new ModelAndView();
		//重定向到查找感兴趣房源列表的动作
		mv.setViewName("redirect:houses.action?userName=" + userName);
		return mv;
	}
	@GetMapping("/houses.action")
	public ModelAndView getHouses(String userName) {
		//这里需要验证用户是否登录,省略
		
		ModelAndView mv = new ModelAndView();
		//查找感兴趣房源并绑定到相应JSP页面,然后将请求转发到该页面
		mv.addObject("mockHouses", houseService.findHousesInterested(userName));
		mv.setViewName("houses.jsp?userName=" + userName);
		return mv;
	}
	@GetMapping("/house-details.action")
	public ModelAndView getHouseDetails(String userName, String houseId) {
		// 这里需要验证用户是否登录,省略
		ModelAndView mv = new ModelAndView();
		//查找房源详情并绑定到相应JSP页面,然后将请求转发到该页面
		mv.addObject("target", houseService.findHouseById(houseId));
		mv.setViewName("house-details.jsp?userName=" + userName);
		return mv;
	}
	@GetMapping("/house-form.action")
	public ModelAndView getHouseForm(String userName, String houseId) {
		// 这里需要验证用户是否登录,省略
		ModelAndView mv = new ModelAndView();
		//查找房源详情并绑定到相应JSP页面,然后将请求转发到该页面
		mv.addObject("target", houseService.findHouseById(houseId));
		mv.setViewName("house-form.jsp?userName=" + userName);
		return mv;
	}
	@PostMapping("/house-form.action")
	public ModelAndView postHouseForm(String userName, String houseId, String houseName, String houseDetail) {
		// 这里需要验证用户是否登录,省略
		//更新指定房源的详情
		House target = houseService.findHouseById(houseId);
		target.setName(houseName);
		target.setDetail(houseDetail);
		//将请求转发到查找房源详情的动作
		ModelAndView mv = new ModelAndView();
		mv.setViewName("redirect:house-details.action?userName=" + userName + "&houseId=" + houseId);
		return mv;
	}
}
           

在Handler方法的取名上,我也是根据HTTP方法加了get、post等前缀。

需要注意的是,处理登录请求和处理房源更新请求这两个Handler最后都采用了重定向。因为它们的目标资源都是GET动作,而这两个请求都是POST请求,如果采用转发,则这两个请求直接交由目标资源的GET动作来处理,这就不匹配了;重定向则是将这两个请求先返回响应给浏览器,浏览器再重新发起对目标资源的GET请求。

验证

我们可以在Eclipse中发布租房网应用到Tomcat服务器,然后启动Tomcat服务器(可以参考这篇文章)。

我们打开浏览器,输入登录页面的URL:

http://localhost:8080/house-renter/login.html
           

登录后即可看到房源列表页面,URL也相应变为:

http://localhost:8080/house-renter/houses.action?userName=a
           

点击某个房源,可以打开该房源的详细信息页面,然后可以进一步编辑该房源的详细信息,提交后,却发现可恶的中文乱码问题再次出现!

中文乱码问题再现

此次出现中文乱码问题还是在提交房源详情请求的处理上,我们可以先通过日志打印的方式把提交的数据(房源名称、详情等)打印出来,看是否是乱码。

果然,在这一步已经是乱码,我们可以大胆猜想Spring MVC和Servlet一样,对数据的编码方式默认都是ISO-8859-1,而String类默认编码和解码是UTF-8。即Spring MVC收到的数据的二进制是ISO-8859-1编码方案的二进制,而输出的时候却使用UTF-8去解码。

所以,我们可以使用以下方式进行转换:

new String(houseName.getBytes("iso-8859-1"), "utf-8")
           

结果还真是解决了乱码问题。唯一的问题是把这个转换代码放到此处有点不太合适,我们是否可以像之前一样使用Filter来解决这种通用维度的问题?

答案当然是可以的。不过这个Filter不需要我们来实现了,Spring MVC已经提供了这么一个Filter,就是:

org.springframework.web.filter.CharacterEncodingFilter
           

我们只需要在部署描述符web.xml中配置它即可:

<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>
           

总结

这次我们真正的把Spring MVC和Spring IoC用到了看起来有用一点的项目上,从代码上来看也还不赖(可以把原来的filter包和servlet包删除),层次还是比较清晰的。

• controller包

• entity包

• service包

• 展示层的HTML页面和JSP页面放在WebContent下

• 配置文件放在WebContent/WEB-INF下

代码也比较干净:

• 展示层和控制器层的数据都通过名字来绑定和解绑,Spring MVC帮我们做了,我们就无需使用getParameter()这样的方法来自己转换了。

• 一些通用维度的功能比如字符编码,Spring MVC也提供了相应的Filter,我们只要配置即可,无需在代码中实现。

• 等等。

当然,也还存在很多改进之处:

• 异常处理还没有;

• 用户登录验证也没有;

• 像房源编辑的数据能否直接绑定到一个House对象中;

• 使用的还是模拟数据;

• 等等。

不过,以后我们再继续介绍其他技术的时候,我们就可以一直使用租房网这个应用,慢慢改进和完善吧。