SpringMVC

1. 作用

原生的JavaEE中,使用Servlet接收并处理请求的,在实际应用中,某个项目中可能涉及许多种请求,例如:用户注册、用户登录、修改密码、修改资料、退出登录等许多功能,在许多情况下,自行创建的每个Servlet只处理1种请求,即对应1个功能,如果项目中有几百个功能,则需要几百个Servlet来处理!进而,就会导致在项目运行过程中,需要几百个Servlet的实例,同时,编写代码时,每个Servlet都需要在web.xml中进行配置,就会产生大量的配置,在代码管理方面,难度也比较大!

使用SpringMVC主要解决了V-C交互问题,即Controller如何接收用户的请求,如何向客户端进行响应,同时,它解决了传统的JavaEE中的问题,每个Controller可以处理若干种不同的请求,且并不存在大量的配置。

2. 练习

1. 目标

用户通过http://localhost:8080/PROJECT/user/reg.do即可访问用户注册页面,并且在该页面中,表单可以将用户的注册数据提交到http://localhost:8080/PROJECT/user/handle_reg.do

同理,通过http://localhost:8080/PROJECT/user/login.do可以访问用户登录页,该页面中可以将用户的登录数据提交到http://localhost:8080/PROJECT/user/handle_reg.do

2. 创建项目

创建新的Maven Project,勾选Create a simple project ...Group Idcn.tedu.springArtifact IdSPRINGMVC-02-UMSPackagingwar,然后点击至创建完成。

首先,项目默认报错,需要通过Eclipse生成web.xml文件。

然后,勾选Tomcat运行环境。

然后,在pom.xml中添加必要的依赖,SpringMVC项目至少需要添加spring-webmvc依赖。

然后,在web.xml中对DispatcherServlet进行配置:

<servlet>
    <servlet-name>SpringMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>SpringMVC</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

最后,在此前的项目中,复制Spring的配置文件到当前项目中来,同时,为了保证项目启动时,就加载该配置文件,应该修改DispatcherServlet的配置,使之成为项目启动时即加载的Servlet,且配置它的启动参数,指定Spring配置文件的路径:

<servlet>
    <servlet-name>SpringMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>SpringMVC</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

3. 显示注册页

创建cn.tedu.spring.UserController类,在类之前添加@Controller,表示这个类是一个控制器组件,同时,为了保证这个类将被Spring容器所管理,还需要在Spring的配置文件中添加组件扫描,扫描该类所在的包:

<!-- 组件扫描 -->
<context:component-scan 
    base-package="cn.tedu.spring" />

如果希望某个类能够被Spring管理,第1种做法是在Spring的配置文件中通过节点进行配置,第2种做法是在Spring的配置文件中添加组件扫描,并在类之前添加@Component/@Controller/@Service/@Repository注解。如果配置自行编写的类,应该使用第2种做法,如果配置是他人编写的类,则应该使用第1种做法。

由于后续涉及的请求有/user/reg.do/user/handle_reg.do等,都是使用/user作为请求路径的前一部分的,所以,应该在类之前添加@RequestMapping("/user")注解。

目前,处理请求的方法应该是: 1. 使用public权限; 2. 使用String类型的返回值(暂定); 3. 方法名称可以自定义; 4. 方法没有参数;

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

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

完成后,可以在浏览器访问http://localhost:8080/SPRINGMVC-02-UMS/user/reg.do,在Eclipse的控制台应该输出UserController.showReg(),则表示/user/reg.do的请求会被以上方法进行处理!

通常使用的视图解析是InternalResourceViewResolver,它将根据配置的前缀+处理请求的方法的返回值+配置的后缀,得到位于webapp下的某个视图文件的路径。

默认情况下,处理请求的方法返回的字符串,表示最终将转发到某个JSP页面,且返回值是JSP的路径中去除视图解析器中前缀与后缀的部分。

假设后续创建的JSP文件是/webapp/WEB-INF/register.jsp,且视图解析器配置的前缀是/WEB-INF/,而后缀是.jsp,则处理请求的方法应该返回"register"

所以,处理请求的方法:

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

并且,在Spring的配置文件中添加视图解析器的配置:

<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/" />
    <property name="suffix" value=".jsp" />
</bean>

基于以上视图解析器的配置,和处理请求的方法的返回值,则需要创建webapp/WEB-INF/register.jsp文件用于显示注册页。

创建完成后,再次访问http://localhost:8080/SPRINGMVC-02-UMS/user/reg.do路径,则可以看到刚才创建的JSP页面。

最后,完善页面,添加注册表单元素:

<form method="post" action="handle_reg.do">
    <div>请输入用户名</div>
    <div><input name="username" /></div>
    <div>请输入密码</div>
    <div><input name="password" /></div>
    <div>请输入年龄</div>
    <div><input name="age" /></div>
    <div>请输入电话</div>
    <div><input name="phone" /></div>
    <div>请输入邮箱</div>
    <div><input name="email" /></div>
    <div><input type="submit" value="注册" /></div>
</form>

4. 处理注册

由于注册信息将提交到http://localhost:8080/PROJECT/user/handle_reg.do,所以,应该在UserController中新添加某个方法,对该路径的请求进行处理:

@RequestMapping("/handle_reg.do")
public String handleReg() {
    return null;
}

【方式1】 通过HttpServletRequest接收请求参数【不推荐】

在处理请求的方法中添加HttpServletRequest参数:

@RequestMapping("/handle_reg.do")
public String handleReg(HttpServletRequest request) {
    String username
        = request.getParameter("username");
    String password
        = request.getParameter("password");
    Integer age
        = Integer.valueOf(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] getParameter()方法返回的是String类型,如果需要的数据类型是其它类型,则需要自行转换;[3] 不便于执行单元测试。

【方式2】 在处理请求的方法中直接添加所需的数据 【推荐】

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

使用时,需要注意的是:参数的名称必须与请求参数的名称保持一致!如果名称不一致,对应的参数将是null值。

这种做法的缺点是:不太适用于请求参数过多的应用场合。

【方式3】 将多个参数封装到某个类中,并使用这个类型作为处理请求的方法的参数 【推荐】

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

使用时,需要注意的是:自定义类中的属性的名称必须与请求参数的名称保持一致!如果名称不一致,对应的属性将是null值。

【小结】 如何接收客户端提交的请求参数

如果客户端提交的请求参数较多,则优先使用第3种方式;

如果客户端提交的请求参数的数量可能发生变化(例如随着需求更新而改版),则优先使用第3种方式;

如果客户端提交的请求参数的数量较少,并且固定,则优先使用第2种方式。

注意:以上2种方式可以组合使用。

关于注册的模拟处理:

@RequestMapping("/handle_reg.do")
public String handleReg(User user, ModelMap modelMap) {
    System.out.println(user);
    // 假设用户名root已经被占用,使用其它数据注册均可以成功
    if ("root".equals(user.getUsername())) {
        // 客户尝试注册已经被占用的用户名
        String message = "您尝试注册的用户名已经被占用!";
        modelMap.addAttribute("msg", message);
        return "error";
    } else {
        // 注册成功:重定向到登录页
        // 当前位置:/user/handle_reg.do
        // 目标位置:/user/login.do
        return "redirect:login.do";
    }
}

【重定向】 当处理请求的方法的返回值以redirect:作为前缀时,表示重定向,在使用重定向时,冒号右侧的部分必须是相对路径或绝对路径,不可以是视图名!

5. 显示登录页

UserController中添加处理请求的方法:

// /user/login.do
@RequestMapping("/login.do")
public String showLogin() {
    System.out.println("UserController.showLogin()");
    return "login";
}

然后,将register.jsp复制粘贴为login.jsp,并调整为:

<h1>用户登录</h1>
<!-- 当前位置:/user/login.do -->
<!-- 目标位置:/user/handle_login.do -->
<form method="post" action="handle_login.do">
    <div>请输入用户名</div>
    <div><input name="username" /></div>
    <div>请输入密码</div>
    <div><input name="password" /></div>
    <div><input type="submit" value="登录" /></div>
</form>

6. 处理登录

UserController中添加处理请求的方法:

@RequestMapping("/handle_login.do")
public String handleLogin(
    String username, String password, ModelMap modelMap) {
    System.out.println("username=" + username);
    System.out.println("password=" + password);
    // 假设admin/123456是正确的用户名与密码,其它的均是错误的
    if ("admin".equals(username)) {
        // 用户名正确,则判断密码
        if ("123456".equals(password)) {
            // 密码正确,登录成功,跳转到主页:/main/index.do
            // 当前位置:/user/handle_login.do
            // 目标位置:/main/index.do
            return "redirect:../main/index.do";
        } else {
            // 密码错误 
            String message = "登录失败!密码错误!";
            modelMap.addAttribute("msg", message);
            return "error";
        }
    } else {
        // 用户名错误 
        String message = "登录失败!用户名不存在!";
        modelMap.addAttribute("msg", message);
        return "error";
    }
}

3. 关于@RequestMapping注解

@RequestMapping注解的主要作用是配置请求路径。该注解可以添加在类之前,也可以添加在某个处理请求的方法之前,当添加在类之前,用于配置路径中的层次(类似于描述本地路径中的文件夹),当添加在方法之前,用于配置类的注解的路径右侧的剩余部分,例如用于配置请求的资源(reg.do或login.do),如果请求的路径之前还有其它路径,也一并配置在这里,例如某请求是http://localhost:8080/PROJECT/user/list/add.do,如果在类之前配置的只有/user,则在方法之前配置为/list/add.do,总的原则就是类之前的注解配置拼接上方法之前的注解配置可以得到完整请求路径即可。

在配置时,最左侧的/是可以忽略的,例如某请求是http://localhost:8080/PROJECT/user/reg.do,在类和方法之前分别配置

/user       /reg.do
user        reg.do
/user       reg.do
user        /reg.do

以上4种配置都是正确的!

关于@RequestMapping注解,还可以用于限制请求类型,当限制了请求类型之后,如果客户端使用错误的方式来提交请求,则会出现405错误,提示内容例如:

Request method 'GET' not supported

实现方式为:

@RequestMapping(value="/handle_login.do", method=RequestMethod.POST)

关于value属性,取值是String[],当只配置1个路径时,直接使用字符串值即可,如果有多个路径,则写成数组格式,例如:

@RequestMapping({"/reg.do", "/register.do"}) 

另有path属性,与value是完全等效的,该属性是从4.2版本开始启用的。

关于method属性,用于设置请求方式,取值为RequestMethod枚举的数组,与value一样,如果只有1个值时可以直接编写,如果有多个值,必须写成数组格式。

@RequestMapping注解相似的还有@GetMapping@PostMapping……这些就是限制了请求类型的@RequestMapping,例如@PostMapping("/handle_login.do")等效于@RequestMapping(value="/handle_login.do", method=RequestMethod.POST),这些注解是从4.3版本开始启用的。

4. 关于@RequestParam注解

@RequestParam是添加在请求参数之前的注解。

通过@RequestParam注解可以解决请求参数名与处理请求的方法的参数名不一致的问题,例如:

@RequestParam("uname") String username

需要注意的是:一旦使用以上注解,默认情况下,该参数是必须提交的!如果客户端没有提交对应的参数,则会导致400错误:

HTTP Status 400 - Required String parameter 'uname' is not present

所以,该注解还可以用于强制要求客户端必须提交某些参数

该注解有属性required,是boolean类型的,表示是否必须的意思,默认值为true

该注解还有属性defaultValue,是String类型的,表示默认值,即客户端的请求中并不包含该参数时,视为提交是某个值!设置默认值时,需要将required显式的设置为false!