当前位置: 代码迷 >> 综合 >> 我的Java Web之路 - Spring MVC和Spring IoC初步使用
  详细解决方案

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

热度:55   发布时间:2023-09-06 13:10:41.0

本系列文章旨在记录和总结自己在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/contexthttps://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 {@Autowiredprivate HouseService houseService;@GetMapping("/test.action")@ResponseBodypublic 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对象中;
? 使用的还是模拟数据;
? 等等。
不过,以后我们再继续介绍其他技术的时候,我们就可以一直使用租房网这个应用,慢慢改进和完善吧。

  相关解决方案