SpringMVC是一个优秀的JavaWeb框架。在正式介绍SpringMVC之前,我们先介绍相关基础知识和JavaWeb的基础。
一、从Java到Tomcat
我们知道,Java分为JavaME(这个主要是做一些嵌入式的程序,我们不关注),JavaSE,JavaEE三大类。JavaSE和JavaEE的区别如下图所示,我们可以得出这个结论:
从涵盖范畴角度看:JavaSE< Web Container < Java EE Webprofile < Java EE full proile(最外侧的框)。
我们在下载SDK的时候,可以看到有Platform SDK和Web Profile SDK。
Web Profile是Java给Web应用程序用的profile,其中不包含Web应用不常使用的组件,如JMS。
那么,现代的Web应用是否使用JavaEE Webprofile呢?大多不是。
大多只使用Web Container。我们接触到的最常见的Web Container就是Tomcat。需要注意的是,Tomcat属于JavaEE,但它只包含部分的JavaEE类库,如Servlet和JSP等。接下来,我们就从Tomcat入手,介绍Web应用的前端。
二、Tomcat
Tomcat目前最近的正式版是Tomcat9。实际上,Tomcat 9 for Linux的安装包非常精简,只有11M,如下所示:
Tomcat的版本发展历史如下:
我们查看安装好的tomcat9的类库(jar包),由于内容较多,只显示主要的:
root@repo lib]# ls -al
-rw-r-----. 1 tomcat tomcat 54692 Nov 12 15:38 catalina-ant.jar
-rw-r-----. 1 tomcat tomcat 63813 Nov 12 15:38 jsp-api.jar
-rw-r-----. 1 tomcat tomcat 75187 Nov 12 15:38 tomcat-i18n-de.jar
-rw-r-----. 1 tomcat tomcat 149747 Nov 12 15:38 tomcat-jdbc.jar
-rw-r-----. 1 tomcat tomcat 234433 Nov 12 15:38 tomcat-websocket.jar
-rw-r-----. 1 tomcat tomcat 40042 Nov 12 15:38 websocket-api.jar
- 在上面的jar包中,我们看到很多catalina开头的。Catalina是Tomcat的servlet容器实现,Catallina和Tomcat的关系大致是发动机和汽车的关系。
- 上面jar包中jsp的jar。
- 中间名为i18n的jar,主要功能是实现语言及地区显示相应的界面的国际化。
Tomcat本身包含Http Server、包含JavaEE中的Servlet、JSP类库(JDBC是JavaSE中的类库)。我们在Linux上安装Tomcat时,需要将Linux自动的Http Server停掉。
三、前端页面:XML与HTML?
XML是W3C指定的一种数据交换格式,它是一种可扩展的标记语言。XML的全称是EXtensible Markup Language的缩写。
使用XML表示树状结构很方便。举个例子,我们可以用xml文件显示城市的层级关系:
层级关系:
用xml书写:
xml有很多语法,这里我们就不展开说了。
HTML也是标记文本,架构与XML类似,都是以标记的形式来描述信息。但两者之间有本质区别,如下:
- XML中的标记用来显示数据;XML中的标记用来描述数据的性质和结构。
- HTML不区分大小写;XML严格区分大小写
- HTML可以有多个根元素,XML只有一个根元素
- HTML中属性的引号可以有客户,XML中的属性必须放在引号中
- HTML中空格自动过滤;XML中空格不自动删除。
- HTML中的标记是预定义的;XML中的标记可以随意定义,并且可扩展。
那么,前端网页是使用HTML还是XML呢?
实际上,现在主流使用的是XHTML。也就是HTML和XML的混合体。它完全采用XML的语法规则来编写Web页面,有效结合HTML的简单性和XML的可扩展性。我们可以将XHTML理解成XML在Web领域的一种应用。
四、B/S架构与Web资源
现在应用大多数都是B/S架构了(而不是C/S架构),如下图所示,这也是比较典型的三层架构:
而浏览器与Web Server的通讯过程,如下图所示:
我们将放在Internet上供外界访问的文件或者程序成为Web资源,根据呈现结果不同,“传统方法”分为:
- 静态资源:Html、CSS、JPG
- 动态资源:JSP、Servlet
在Web前端的开发中,动态页面的实现很重要。以前主要采用JSP/Servlet的方式,现在更多的采用动态HTML的方式。这两种方式的本质区别是:
JSP的动态效果实现,是由服务器端的程序实现的。而动态HTML,是浏览器侧执行脚本实现的。
如下案例所示。
下面第一个是通过动态显示日期,是通过浏览器执行脚本的结果。
下面第二个是通过java程序实现的。
五、Servlet与JSP
如前文所述,Servlet主要是为了实现Web的动态资源。而JSP(全称是Java Server Page),是建立在Servlet标准之上,为了解决Servlet在实现动态页面繁琐的问题而产生的,它也是动态页面的开发技术。
JSP在开发Web应用时,有两种架构:JSP Model1和Model2。
JSP Model1采用JSP+JavaBean的技术,将页面显示与业务逻辑分开。JSP实现流程控制和页面显示,JavaBean独享封装数据和业务逻辑,如下图所示:
通过上面这种模式,一定成熟上实现了程序开发的模块化,实现了业务逻辑和页面显示的分离。
Java为Servlet提供了一系列的接口和类,其中最重要的就是javax.servlet.Servlet接口。它是由Web容器负责创建并调用,用于接收和响应用户的请求。具体代码我们不展开介绍。
Servlet的生命周期如下:
浏览器访问Servlet的过程如下:
JSP Model1虽然实现了数据和部分业务逻辑从JSP页面分离,但JSP页面仍然需要负责流程控制和产生用户界面。结果是:JSP页面中依旧会嵌入大量的Java代码。因此当时Sun提出了JSP Model2。
Model2采用JSP+Servlet+JavaBean技术。它将原本的JSP页面中的流控提取出来,封装到Servlet中,从而实现整个程序页面显示、流程控制和业务逻辑的分离。
Model2就是MVC的设计模式。其中控制器的角色由Servlet实现、视图由JSP页面实现、模型的角色由JavaBean实现。
六、微服务时代的前端
JSP曾经风靡一时,在微服务时代,用的多么?
答案是:不多。
JSP的问题在哪?
前后端的开发不能分离!
在微服务时代,如果不能实现前后端的开发分析,那将是一种灾难。通过将全后端的开发分析后,前端只负责展现和交互,后端负责核心业务逻辑。前后端通过API进行交互,并且最好符合RESTful风格。服务器端把数据返回给前端就不再关心这些数据用在哪里、如何布局、什么样式。
现在主流的前端开发框架是AngulaJS(2009年诞生)。Angular是一个谷歌维护的JavaScript MVC框架,一款为了克服HTML在构建应用上的不足而设计的优秀的前端JS框架,用于开发动态Web应用程序。
AngulaJS 等这些前端的流行主要是随着客户端计算资源的强大,浏览器客户端可以进行大量的计算,降低和服务器端的调用次数,客户端和服务器端做到相对解耦
Angular是一个MVC框架
1. Model模型 主要是负责业务数据的处理,前端项目中的表现为js变量
2. View视图 主要是业务数据在用户面前显示和数据的收集,前端项目中的表现为html代码。
3. Controller控制器 主要负责是业务数据的curd操作【操作模型里面的数据,调用模型来对数据处理】,协调模型和视图之间的关系,前端项目中的表现为function。
Angular适于开发客户端的单页面应用。它不是个功能库,而是用来开发动态网页的框架。它专注于扩展HTML的功能,提供动态数据绑定(data binding),而且它能跟其它框架(如jQuery)合作融洽。
Angular依然可以运行在Tomcat上。Http server、Web container 都运行在服务端,客户端都需要通过浏览器访问,浏览器访问的时候会下载一些静态的资源(.html,. js)到客户端浏览器,下载到的.js .html会和后端java 代码进行通信,AngulaJS 就是浏览器会下载到客户端的静态资源。我们在客户端浏览器上的 AngulaJS,会通过点击按钮或等触发服务器调运,通常会返回一个 JSON 文件,AngulaJS 会将返回的 JSON 进行渲染展现成漂亮的界面。如下图所示。
传统单页面模式:
传统多页面模式:
我们知道REST 本来就是无状态的,AngulaJS + REST 返回 JSON,这提高了整体前后端的开发效率。实现了前端和后端解耦。
目前还有很多前端的开发框架,偏传统应用开发,还是 AngulaJS 多。
例如 node.js 等直接在前端完成 MVC,建模控制都在前端,后端单纯的业务逻辑处理,node.js + mongodb 战斗力远大于 JSP + Java 中间件 + Mysql/Oracle。具体内容不站看说明。
七、被前后夹击的JSP
上面我们提到,Java Web前端现在已经抛弃JSP。但Java Web后端其实也在抛弃JSP。
我们知道,在微服务时代,传统的Java系被大量使用的框架,主要是SpringBoot。从Spring 5开始,在原有的基于Servlet技术的Spring MVC之外增加了一个新的编程模型,就是Spring WebFlux。
Spring WebFlux 是 Spring Framework 5.0中引入的新的响应式web框架。与Spring MVC不同,它不需要Servlet API,是完全异步且非阻塞的,并且通过Reactor项目实现了Reactive Streams规范。Spring WebFlux 不支持Servlet API/JSP。当然,我们还可以用传统的spring-webmvc模式。
新旧两种模式的技术栈:
【spring-webmvc + Servlet/JSP + Tomcat】命令式的、同步阻塞的
【spring-webflux + Reactor + Netty】响应式的、异步非阻塞的
但是,SpringBoot官方是建议能不用JSP就不用JSP。
上面我们提到了Nettry。Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。
八、SpringMVC的实现
SpringMVC的工作原理如下图所示:
1.客户端请求提交到DispatcherServlet;
2.由DispatcherServlet控制器寻找一个或多个HandlerMapping,找到处理请求的Controller;
3.DispatcherServlet将请求提交到Controller;
4.Controller调用业务逻辑处理后,返回ModelAndView;
5.DispatcherServlet寻找一个或多个ViewResoler视图解析器,找到ModelAndView指定的视图;
6.视图负责将结果显示到客户端。
SpringMVC所需要的JAR包
对于SpringMVC框架的初学者,开发SpringMVC应用时,只需要将Spring的四个基础包、commons-logging-1.2.jar、注解时需要的JAR包spring-aop-5.1.4.RELEASE.jar和Spring MVC相关的JAR包(spring-web-5.1.4.RELEASE.jar和spring-webmvc-5.1.4.RELEASE.jar)复制到Web应用的WEB-INF/lib目录下即可。
SpringMVC的Web应用有三种开发方式:
方法1:使用Eclipse开发SpringMVC的Web应用
- 1.创建Web应用ch2_1并导入JAR包
- 2.在web.xml文件中部署DispatcherServlet
- 3.创建Web应用首页
- 4.创建Controller类
- 5.创建SpringMVC的配置文件
- 6.应用的其他页面
- 7.发布并运行Spring MVC应用
方法2:基于Java配置的SpringMVC应用
在【例2-1】中,我们使用web.xml和springmvc-servlet.xml配置文件进行Web配置和Spring MVC配置。但SpringBoot推荐使用Java配置的方式进行项目配置,因此,本小节通过一个实例来演示Spring MVC应用的Java配置。【例2-2】在【例2-1】的基础上实现,具体步骤如下。
- 1.创建Web应用ch2_2并导入JAR包
- 2.复制JSP和Java文件
- 3.创建SpringMVC的Java配置(相当于springmvc-servlet.xml文件)
- 4.创建Web的Java配置(相当于web.xml文件)
- 5.发布并运行Spring MVC应用
方法3:基于注解控制器实现Spring MVC Web。
本方法是Spring中的重点,其涵盖如下知识点:
在Spring MVC中,使用org.springframework.stereotype.Controller注解类型声明某类的实例是一个控制器。例如,2.2.2节中的IndexController控制器类。别忘了在Spring MVC的配置文件中使用元素(见【例2-1】)或在Spring MVC配置类中使用@ComponentScan(见【例2-2】)指定控制器类的基本包,进而扫描所有注解的控制器类。
在基于注解的控制器类中,可以为每个请求编写对应的处理方法。需要使用org.springframework.web.bind.annotation.RequestMapping注解类型将请求与处理方法一一对应。
1.请求处理方法中常出现的参数类型
Servlet API、输入输出流、表单实体类、注解类型、Model等Java类型。
2.请求处理方法常见的返回类型
最常见的返回类型,就是代表逻辑视图名称的String类型。除了String类型外,还有Model、View以及其他任意的Java类型。
Controller接收请求参数的方式有很多种,有的适合get请求方式,有的适合post请求方式,有的两者都适合。
1.通过实体bean接收请求参数
通过一个实体bean来接收请求参数,适用于get和post提交请求方式。需要注意的是,bean的属性名称必须与请求参数名称相同。见【例2-3】
2.通过处理方法的形参接收请求参数
通过处理方法的形参接收请求参数,也就是直接把表单参数写在控制器类相应方法的形参中,即形参名称与请求参数名称完全相同。该接收参数方式适用于get和post提交请求方式。见【例2-3】
3.通过@RequestParam接收请求参数
通过@RequestParam接收请求参数,适用于get和post提交请求方式。通过@RequestParam接收请求参数与“通过处理方法的形参接收请求参数”的区别是:当请求参数与接收参数名不一致时,“通过处理方法的形参接收请求参数”不会报400错误,而“通过@RequestParam接收请求参数”会400错误。
4.通过@ModelAttribute接收请求参数
@ModelAttribute注解放在处理方法的形参上时,用于将多个请求参数封装到一个实体对象,从而简化数据绑定流程,而且自动暴露为模型数据用于视图页面展示时使用。而“通过实体bean接收请求参数”只是将多个请求参数封装到一个实体对象,并不能暴露为模型数据(需要使用model.addAttribute语句才能暴露为模型数据)
重定向与转发
//转发到一个请求方法(同一个控制器类里,可省略/index/)
return "forward:/index/isLogin";
//重定向到一个请求方法
return "redirect:/index/isRegister";
//转发到一个视图
return "register";
通过org.springframework.web.bind.annotation.ModelAttribute注解类型,可经常实现如下两个功能:
1.绑定请求参数到实体对象(表单的命令对象)
publicString register(@ModelAttribute("user")UserFormuser) {}
“@ModelAttribute(”user“)UserFormuser”语句的功能有两个,一是将请求参数的输入封装到user对象中;一是创建UserForm实例,以“user”为键值存储在Model对象中,与“model.addAttribute(”user“,user)”语句功能一样。如果没有指定键值,即“@ModelAttributeUserForm user”,那么创建UserForm实例时,以“userForm”为键值存储在Model对象中,与“model.addAttribute(”userForm“,user)”语句功能一样。
2.注解一个非请求处理方法
被@ModelAttribute注解的控制器的一个非请求处理方法,将在每次调用该控制器类的请求处理方法前被调用。
九、案例展现:基于注解控制器实现Spring MVC Web
创建Spring MVC的Java配置(相当于springmvc-servlet.xml文件):
package config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.web.multipart.MultipartResolver;import org.springframework.web.multipart.commons.CommonsMultipartResolver;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import org.springframework.web.servlet.view.InternalResourceViewResolver;import interceptor.MyInteceptor;@Configuration@EnableWebMvc@ComponentScan(basePackages = {"controller","service"})public class SpringMVCConfig implements WebMvcConfigurer {/** * 配置视图解析器 */@Beanpublic InternalResourceViewResolver getViewResolver() {InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();viewResolver.setPrefix("/WEB-INF/jsp/");viewResolver.setSuffix(".jsp");return viewResolver;}/** * 配置静态资源 */@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/html/**").addResourceLocations("/html/");//addResourceHandler指的是对外暴露的访问路径//addResourceLocations指的是静态资源存放的位置}/** * 配置拦截器Bean */@Beanpublic MyInteceptor myInteceptor() {return new MyInteceptor();}/** * 重写addInterceptors方法注册拦截器 */@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(myInteceptor());}/** * MultipartResolver配置 */@Beanpublic MultipartResolver multipartResolver() {CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();//设置上传文件的最大值,单位为字节multipartResolver.setMaxUploadSize(5400000);//设置请求的编码格式,默认为iso-8859-1multipartResolver.setDefaultEncoding("UTF-8");return multipartResolver;}}
创建Web的Java配置(相当于web.xml文件)
package config;import javax.servlet.ServletContext;import javax.servlet.ServletException;import javax.servlet.ServletRegistration.Dynamic;import org.springframework.web.WebApplicationInitializer;import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;import org.springframework.web.servlet.DispatcherServlet;public class WebConfig implements WebApplicationInitializer{@Overridepublic void onStartup(ServletContext arg0) throws ServletException {AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();ctx.register(SpringMVCConfig.class);//注册Spring MVC的Java配置类SpringMVCConfigctx.setServletContext(arg0);//和当前ServletContext关联/** * 注册Spring MVC的DispatcherServlet */Dynamic servlet = arg0.addServlet("dispatcher", new DispatcherServlet(ctx));servlet.addMapping("/");servlet.setLoadOnStartup(1);}}
查看UserController
package controller;import javax.servlet.http.HttpSession;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.RequestMapping;import pojo.UserForm;import service.UserService;@Controller@RequestMapping("/user")public class UserController {//得到一个用来记录日志的对象,这样打印信息的时候能够标记打印的是那个类的信息private static final Log logger = LogFactory.getLog(UserController.class);//将服务层依赖注入到属性userService@Autowiredpublic UserService userService;/** * 处理登录 */@RequestMapping("/login")public String login(UserForm user, HttpSession session, Model model) {if(userService.login(user)){session.setAttribute("u", user);logger.info("成功");return "main";//登录成功,跳转到main.jsp}else{logger.info("失败");model.addAttribute("messageError", "用户名或密码错误");return "login";}}/** *处理注册 */@RequestMapping("/register")public String register(@ModelAttribute("user") UserForm user) {if(userService.register(user)){logger.info("成功");return "login";//注册成功,跳转到login.jsp}else{logger.info("失败");//使用@ModelAttribute("user")与model.addAttribute("user", user)功能相同//在register.jsp页面上可以使用EL表达式${user.uname}取出ModelAttribute的uname值return "register";//返回register.jsp}}}
MyInteceptor.java
package interceptor;import javax.servlet.http.HttpServletRequest;public class MyInteceptor implements HandlerInterceptor{/** * 重写preHandle方法在请求发生前执行 */@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {System.out.println("preHandle方法在请求发生前执行");return true;}/** * 重写postHandle方法在请求完成后执行 */@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {System.out.println("postHandle方法在请求完成后执行");}}
UserForm.java
package pojo;public class UserForm {private String uname;//与请求参数名称相同private String upass;private String reupass;public String getUname() {return uname;}public void setUname(String uname) {this.uname = uname;}public String getUpass() {return upass;}public void setUpass(String upass) {this.upass = upass;}public String getReupass() {return reupass;}public void setReupass(String reupass) {this.reupass = reupass;}}
UserService接口类
package service;import pojo.UserForm;public interface UserService {boolean login(UserForm user);boolean register(UserForm user);}
接口实现类UserServiceImpl
package service;import org.springframework.stereotype.Service;//注解为一个服务@Servicepublic class UserServiceImpl implements UserService{@Overridepublic boolean login(UserForm user) {if("zhangsan".equals(user.getUname()) && "123456".equals(user.getUpass()))return true;return false;}@Overridepublic boolean register(UserForm user) {if("zhangsan".equals(user.getUname()) && "123456".equals(user.getUpass()))return true;return false;}}