Spring MVC 控制器初识

在 Spring MVC 中控制器的主要作用就是绑定请求参数处理业务逻辑返回模型数据和视图,要定义一个控制器也很简单,使用@Controller注解标注一个类即可,但这还不够,还需要结合@RequestMapping注解,它可以在类、方法上使用,用来指定请求 URL 可以由控制类中的那个方法来处理,这里我们主要学习以下两方面的内容:

  • 绑定请求参数
  • 模型数据和视图
  • 重定向和转发

一、绑定请求参数

1、普通请求参数

在 Spring MVC 中,如果网络请求时传递的参数名称和控制器方法的形参名称一致,则对应的参数值会自动绑定到控制器方法的形参上。

先准备一个表单 RoleForm.jsp 来提交参数:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>RoleForm</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/role/commonParams" id="form" method="post">
    <table>
        <tr>
            <td>名称</td>
            <td><input id="roleName" name="roleName" value=""/></td>
        </tr>
        <tr>
            <td>备注</td>
            <td><input id="note" name="note" value=""/></td>
        </tr>
        <tr>
            <td></td>
            <td align="right"><input id="commit" type="submit" value="提交"/></td>
        </tr>
    </table>
</form>
</body>
</html>

要提交的两个参数名称分别为roleNamename,地址是http://localhost:8080/role/commonParams,再看对应的控制器类:

@Controller
@RequestMapping("/role")
public class RoleController {
 
    @RequestMapping("/roleForm")
    public String roleForm() {
        return "roleForm";
    }

    @RequestMapping("/commonParams")
    public ModelAndView commonParams(String roleName, String note) {
        ModelAndView mv = new ModelAndView();
        Role role = new Role();
        role.setRoleName(roleName);
        role.setNote(note);
        mv.addObject("role", role);
        mv.setViewName("result");
        return mv;
    }
}

形参名和表单提交的参数名一致,这样当提交表单后,就可以直接通过形参拿到参数值,同时将请求转发到result.jsp来展示提交的数据,结果如下:

1

2

如果提交的参数名称和控制器方法的形参不一致,则是无法将参数值绑定到形参上的,同时又无法修改提交的参数名称。例如将 RoleForm.jspname="roleName"改为name="role_name",上边的控制器方法自然是无法正常获取参数值的,要解决这个问题可以使用@RequestParam注解,它的作用是将请求参数的值赋给形参:修改后的控制器方法如下:

@RequestMapping("/commonParams2")
public ModelAndView commonParams2(@RequestParam("role_name") String roleName, String note) {
    return commonParams(roleName, note);
}

这样问题就解决了,要注意的是使用了@RequestParam("role_name")role_name参数的值默认不能为空,否则会有异常,如果不能保证role_name有值,可以配置required属性:

@RequestMapping("/commonParams2")
public ModelAndView commonParams2(@RequestParam(value = "role_name", required = false) String roleName, String note) {
    return commonParams(roleName, note);
}

如果表单提交的参数过多,那么控制器方法的形参也会很多,这样大大降到底了代码的可读性、增加了后期的维护成本,当然 Spring MVC 早已想到了这一点,我们可以将控制器的方法参数声明为一个对象,只要其包含的属性名称和请求参数的名称一致即可,就会得到一个属性值为提交参数的对像。

修改 RoleForm.jspaction属性为${pageContext.request.contextPath}/role/commonParams3,对应的控制方法如下:

@RequestMapping("/commonParams3")
public ModelAndView commonParams3(Role role) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("role", role);
    mv.setViewName("result");
    return mv;
}

2、URL 模板参数

RESTful API 的设计规范中,可以通过 URL 来传递参数,例如要通过 id 获得某个角色的信息,并用 JSON 格式返回,我们希望 URL 是这样的:http://localhost:8080/role/getRole/1,其中 1 是角色 id,动态变化的。当然 Spring MVC 也支持这样的情况,通过@PathVariable注解可以方便的获取请求 URL 的动态参数,对应的控制器方法如下:

@RequestMapping("/getRole/{id}")
@ResponseBody
public Role getRole(@PathVariable("id") long id) {
    Role role = roleService.getRole(id);
    return role;
}

@ResponseBody注解可以将返回的对象转换成 JSON 格式,在浏览器测试效果如下:

3

3、JSON 格式参数

如果参数结构比较复杂,为了方便而使用 JSON 格式传递数据也是比较常见的,接下来实现 JSON 数据的提交和解析。首先准备 RoleForm2.jsp,主要的作用是将表单数据已 JSON 格式提交:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>RoleForm2</title>
    <script src="${pageContext.request.contextPath}/js/jquery-3.4.1.min.js"></script>
    <script>
        $(function () {
            $("#commit").click(function () {
                var data = {
                    id: 1,
                    roleName: $("#roleName").val(),
                    note: $("#note").val()
                }
                
                $.post({
                    url: "./jsonParams",
                    contentType: "application/json",
                    data: JSON.stringify(data),
                    success: function (result) {
                        alert("名称:" + result.roleName + "\n" + "备注:" + result.note)
                    }
                });
            });
        });
    </script>
</head>
<body>
<form id="form">
    <table>
        <tr>
            <td>名称</td>
            <td><input id="roleName" name="roleName" value=""/></td>
        </tr>
        <tr>
            <td>备注</td>
            <td><input id="note" name="note" value=""/></td>
        </tr>
        <tr>
            <td></td>
            <td align="right"><input id="commit" type="button" value="提交"/></td>
        </tr>
    </table>
</form>
</body>
</html>

这里通过 jQuery 以 JSON 格式来提交数据到http://localhost:8080/role/jsonParams,并在 success回调中用一个弹窗回显提交的数据,控制器方法如下:

@Controller
@RequestMapping("/role")
public class RoleController {
    @RequestMapping("/roleForm2")
    public String roleForm2() {
        return "roleForm2";
    }

    @RequestMapping("/jsonParams")
    @ResponseBody
    public Role jsonParams(@RequestBody Role role) {
        return role;
    }
}

这里的重点是jsonParams方法中的@RequestBody注解,它会将 JSON 串自动映射为对象的,当然 JSON 串中的属性名要和对象中属性名保持一致。同时jsonParams方法将提交的数据以 JSON 格式返回,以便在弹窗中获取其属性值。测试效果如下:

4

同样的原理,我们可以将列表数据以 JSON 的格式提交,具体代码可参考 demo。

4、序列化参数

如果表单数据比较复杂,我们自己收集数据的话会比较麻烦,一种简便的方式就是将表单序列化。我们修改 RoleForm2.jsp 提交按钮的事件:

$("#commit").click(function () {
    //提交表单
    var data = $("form").serialize();
    $.post({
        url: "./serializeParams",
        data: data,
        success: function (result) {
              var s =  $.parseJSON(result);
              alert("名称:" + s.roleName + "\n" + "备注:" + s.note)
        }
    });
});

其中$("form").serialize()就是将表单的数据序列化,返回一个字符串,得到的参数如下:roleName=管理员&note=666

对应的控制器方法为:

@RequestMapping("/serializeParams")
@ResponseBody
public Role serializeParams(Role role) {
    return role;
}

执行效果和图4类似。

同样可以将表单数据序列化成 JSON 数组格式,使用如下 jQuery 方法即可:$("form").serializeArray(),得到的 JSON 数据格式参数如下:

[{
    "name": "roleName",
    "value": "管理员"
}, {
    "name": "note",
    "value": "666"
}]

二、模型数据和视图

其实我们在绑定请求参数部分内容中,已经接触到了模型数据和视图,在 Spring MVC 中两者一般是搭配使用的,模型数据一般保存控制器方法中执行业务后需要展示或返回的数据,然后将模型数据渲染到视图中。

1、模型数据

常用的模型数据有ModelModelMapModelAndView。Spring MVC 在控制器方法被调用前会创建一个隐含的BindingAwareModelMap类型的模型数据,这样如果在控制器方法中声明了ModelModelMapModelAndView其中某个类型的形参,则 Spring MVC 会将隐含的模型数据转换为声明的模型数据类型,即自动创建了形参的实例,当然你也可以选择手动创建。有了模型数据的实例,接下来就可以添加数据了,之后可以在视图页面得到添加的数据。模型数据以键值对的形式来添加数据。

2、视图

视图分为逻辑视图非逻辑视图,怎么理解呢?

  • 逻辑视图,需要通过视图解析器解析逻辑视图名,得到真实的视图,然后渲染模型数据,例如InternalResourceView,视图解析器以及逻辑视图的文件目录在dispatcher_servlet.xml中已经配置过了。
  • 非逻辑视图,无需视图解析器,直接渲染模型数据,例如MappingJackson2JsonView
2.1

使用逻辑视图时,控制器方法的返回值可以是String类型,即逻辑视图名,这样 Spring MVC 会根据逻辑视图名以及配置的视图解析器的视图文件目录和文件后缀名找到对应的视图文件,得到真实的视图,如果有模型数据则渲染数据,将最终的视图呈现给用户。看如下的例子:

@Controller
@RequestMapping("/role")
public class RoleController {
    @Autowired
    private RoleService roleService;

    @RequestMapping("/getRoles2")
    public String getRoles2(Model model) {
        List<Role> roles = roleService.getAllRole();
        model.addAttribute("roles", roles);
        return "role";
    }
}

控制器方法getRoles2返回逻辑视图名role,所以需要提前在/WEB-INF/JSP/目录下定义role.jsp文件。模型数据model中保存了角色的集合数据。所以在jsp文件中根据角色数据渲染一个表格:

<c:forEach items="${roles}" var="role">
        <tr>
            <td><c:out value="${role.id}"/></td>
            <td><c:out value="${role.roleName}"/></td>
            <td><c:out value="${role.note}"/></td>
        </tr>
    </c:forEach>

运行后可以看到如下效果:


5
2.2

控制器方法除了返回String,还可以返回ModelAndViewModelAndView既可以添加数据也可以设置逻辑视图名,对应的控制器方法如下:

@RequestMapping("/getRoles")
public ModelAndView getRoles(ModelAndView mv) {
    List<Role> roles = roleService.getAllRole();
    mv.setViewName("role");
    mv.addObject("roles", roles);
    return mv;
}

运行效果和图5一致。

2.3

这里补充一点,如果在控制器方法中声明了Model或者ModelMap参数,同时声明了一个类的对象参数,例如Role来接收请求参数,那么Model或者ModelMap参数会自动添加声明的Role对象,添加数据时使用的默认 key 就是类首字母小写的字符串,即role,是由框架自动推断出的。

2.4

使用非逻辑视图时,例如返回 JSON 格式的数据,由于 Spring MVC 默认使用 Jackson 处理 JSON 数据,所以可以先用MappingJackson2JsonView视图来完成数据格式的转换:

@RequestMapping("/getRoles3")
public ModelAndView getRoles3() {
    ModelAndView mv = new ModelAndView();
    List<Role> roles = roleService.getAllRole();
    mv.addObject("roles", roles);
    mv.setView(new MappingJackson2JsonView());
    return mv;
}

当然,要返回 JSON 格式数据,更简单的方式是使用@ResponseBody,但本质上还是采用 Jackson 处理 JSON 数据,当然也可以整合其它的 JSON 处理框架,例如Fastjson,这样当使用@ResponseBody@RequestBody注解时会使用 Fastjson 来处理 JSON 数据。整合的方式也很简单,使用 Fastjson 提供的FastJsonHttpMessageConverter来替换 Spring MVC 默认的 HttpMessageConverter即可,在dispatcher_servlet.xml添加如下配置

<mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html;charset=UTF-8</value>
                        <value>application/json;charset=UTF-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

使用@ResponseBody返回 JSON 的控制器如下:

@RequestMapping("/getRoles4")
@ResponseBody
public List<Role> getRoles4() {
    List<Role> roles = roleService.getAllRole();
    return roles;
}

浏览器输入http://localhost:8080/role/getRoles4,效果如下:

6

三、重定向和转发

首先通过一个表格来了解下重定向和转发之间的差别:

重定向(redirect) 转发(forward)
执行重定向会重新发起新请求 执行转发依然是上次的请求
地址栏的请求URL会变为新请求的URL 地址栏的请求URL不会改变
由于重新发起请求,原请求的请求参数、request范围内的属性全部丢失 原请求的请求参数、request范围内的属性依然存在

1、重定向(redirect)

在 Spring MVC 中,当控制器方法返回的字符串带有redirect:前缀时,那么该字符串不是用来查找视图的逻辑视图名,而是要执行重定向,我们这里所说的重定向仅代表重定向到控制器的请求处理方法。例如"redirect:./result",代表重定向到./result路径对应的控制器方法。其实要实现重定向很简单,关键是如何传递参数。

1.1

如果要传递参数到目标控制器方法,可以采用model.addAttribute方式添加参数:

@RequestMapping("/addRole")
public String addRole(Model model, Role role) {
    model.addAttribute("roleName", role.getRoleName());
    model.addAttribute("note", role.getNote());
    return "redirect:./result";
}

目标控制器方法很简单,仅仅是展示传递的参数:

@RequestMapping("/result")
public String result(Model model, Role role) {
    return "result";
}

大致的流程是,通过表单将参数提交到addRole控制器方法,然后传递参数重定向到result控制器方法来展示提交的参数。在浏览器执行后发现,如果按照上述方式添加重定向参数,则在重定向时参数会以查询参数的形式拼接到 URL 上,例如:http://localhost:8080/role/result?roleName=%E7%AE%A1%E7%90%86%E5%91%98&note=666

1.2

由于通过model.addAttribute添加重定向参数时,参数会拼接在 URL 上,这样存在安全性问题。我们可以考虑另外一种方式,就是我们之前学习的通过 URL 模板传递参数,代码如下:

@RequestMapping("/addRole2")
public String addRole2(Model model, Role role) {
    model.addAttribute("roleName", role.getRoleName());
    model.addAttribute("note", role.getNote());
    return "redirect:./result2/{roleName}";
}

model 中的roleName会作为参数填充到 URL 中,note由于和 URL 模板参数不匹配则会以查询参数的形式拼接到 URL 上。

要重定向到的控制器方法如下,用来接收 URL 模板参数和查询参数:

@RequestMapping("/result2/{roleName}")
public String result2(Model model, @PathVariable("roleName") String roleName, String note) {
    model.addAttribute("roleName", roleName);
    model.addAttribute("note", note);
    return "result2";
}

重定向时的 URL 如下:http://localhost:8080/role/result2/%E7%AE%A1%E7%90%86%E5%91%98?note=666,可以和第一种方式对比下。

1.3

上边两种方式只能添加简单的基本类型参数,例如字符串、数字等,如要传递对象就无能为力了,当然 Spring MVC 自然有解决方案,我们可以采用另外一个模型数据RedirectAttributes来添加 flash 类型的参数,在执行重定向前,会将参数复制到会话(Session)中,当重定向后,从会话中取出参数。

添加参数的方式如下:

@RequestMapping("/addRole3")
public String addRole3(RedirectAttributes ra, Role role) {
    ra.addFlashAttribute("role", role);
    return "redirect:./result3";
}

重定向后,参数值会自动定转存到 model 中,当然也可以定义 Role 对象来接收参数值。

@RequestMapping("/result3")
public String result3(Model model) {
    return "result";
}

重定向的 URL 如下:http://localhost:8080/role/result3,由于是通过会话保存参数的,并不会将参数值暴露,所以也更加的安全。

1.4

上边的例子中,我们的重定向是在同一个控制器中进行的,更多的时候可能需要重定向到另一个控制器的请求处理方法,我们来修改1.3的例子,要重定向到的控制器类的方法如下:

@Controller
@RequestMapping("/result")
public class ResultController {
    @RequestMapping("/success")
    public String result(Role role) {
        return "result";
    }
}

编写addRole4方法来重定向到上边的控制器方法:

@RequestMapping("/addRole4")
public String addRole4(RedirectAttributes ra, Role role) {
    ra.addFlashAttribute("role", role);
    return "redirect:/result/success";
}

注意返回值,即多了一个控制器类的根路径/result,少了.符号。

1.5

上边的例子重定向时都是返回以redirect:开头的字符串,当然也可以使用ModelAndView,例如:mv.setViewName("redirect:/result/success")

2、转发(forward)

2.1

和重定向类似, 在 Spring MVC 中,当控制器方法返回的字符串带有forward:前缀时,那么就是要执行请求转发操作,我们之前接触到的返回一个逻辑视图名,来展示相应的jsp页面,就是一种转发,只是默认省略了forward:。这里我们要讨论的控制器方法之间的转发。先看个例子:

@RequestMapping("/addRole5")
public String addRole5(Model model, Role role) {
    model.addAttribute("roleName", role.getRoleName());
    return "forward:./fail";
}

addRole5控制器方法会直接将请求转发到当前控制下可以相应fail请求的控制器方法,提示当前添加的角色已存在:

@RequestMapping("/fail")
public String fail() {
    return "fail";
}
2.2

如果控制器方法返回ModelAndView,也是可以进行请求转发的:

@RequestMapping("/addRole6")
public ModelAndView addRole6(ModelAndView mv, Role role) {
    mv.addObject("roleName", role.getRoleName());
    mv.setViewName("forward:./fail");
    return mv;
}
2.3

当然也可以转发到另一个控制器方法:

@RequestMapping("/addRole7")
public ModelAndView addRole7(ModelAndView mv, Role role) {
    mv.addObject("roleName", role.getRoleName());
    mv.setViewName("forward:/result/fail");
    return mv;
}

关键的代码就是mv.setViewName("forward:/result/fail"),要转发的目标控制器方法如下:

@Controller
@RequestMapping("/result")
public class ResultController {
    @RequestMapping("/fail")
    public String fail() {
        return "fail";
    }
}

文中demo地址:https://github.com/SheHuan/MyJavaEE

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269