学编程,来源栈;先学习,再交钱
SpringMVC:ModelAndView / MockService / model绑定 / freemarker标签库

当前系列: Java Web开发 修改

ModelAndView

顾名思义,该对象应包含两部分内容:

  • Model:ModelMap(继承自LinkedHashMap<String, Object>)类型的数据容器
  • View:实际上可以是一个viewname,或者模板路径

由标记了@RequestMapping的Handler method返回,交ViewResolver解析,生成HTML内容

演示:查看源代码

public ModelAndView Single() {
	ModelAndView mv = new ModelAndView();
	mv.setViewName("article/single");
	mv.addObject("title", "源栈课堂 之 SpringMVC入门");
	return mv;
}

Model中存的数据,最终都会存放到HttpServletRequest对象中,页面上可以通过HttpServletRequest对象获取数据。

在模板中就可以直接使用:

<h1>${title}</h1>

对象

也可以讲一个对象直接传递到model中
Article ariticle = new Article();
ariticle.setTitle("SpringMVC入门(1)");
article.setBody("ModelAndView\\r\\n顾名思义,该对象应包含两部分内容: ……");
mv.addObject("article",ariticle);

注意:这个Article对象必须是public的!

package models;

public class Article {
	private String title;
	private String body;

在freemarker的模板中,可以用点(.)引出对象属性:

<h3>${article.title}</h3>
<article>${article.body}</article>

变型记

在handler method的参数中添加org.springframework.ui.Model

public String Single(Model model) {
    model.addAttribute("title", "SpringMVC入门(1)");
    return "article/single";
}

还可以是:

  • org.springframework.ui.ModelMap
  • java.uti.Map

理解:(堆栈调用代码演示)

  • Single()方法由SpringMVC框架(使用反射)调用,所以
  • model对象由SpringMVC框架提供
  • Model只是一个接口,SpringMVC会提供具体的实现类,比如:BindingAwareModelMap
  • 因为使用的是反射,所以无论是参数还是返回值,框架都可以直接获取,用于后续页面的生成


MockService

复习:分层架构 / entity&Repository / AOP&IoC

我们开始逐步引入架构Service层。

首先要(通过和BLL层开发团队等协商)定义一个接口,我们把所有的Service层接口专门放一个项目ServiceInterface中:

public interface IArticleService {
	ArticleModel GetById(int id);

ArticleModel属于MVC中的Model,也存放在单独的项目ViewModel中

public class ArticleModel {
	private String title;
因为分属于不同的两个项目,这就要在ServiceInterface中添加对ViewModel的引用:项目上Alter+Enter,进入其properties,在Java Build Path的projects选项下Add

此后其他项目的添加引用以此为例,不再赘述。

然后在MockService中模拟实现:

public class ArticleService implements IArticleService {
	@Override
	public ArticleModel GetById(int id) {
		ArticleModel model = new ArticleModel();
		if (id==1) {
			model.setTitle("SpringMVC:Dispatch / @Controller / @RequestMapping ");
			model.setBody("……");
		}else if (id==2) {
			model.setTitle(" JSP页面:变量赋值 / 表达式 / 分支循环 / include / useBean / 底层servlet");
			model.setBody("……");
		}
		return model;
	}

最后在ArticleController中调用MockService的方法GetById()方法:

public String Single(Model model, @PathVariable int id) {
    model.addAttribute("article", service.GetById(id));

为了:

  • 在多个handler methods中重用
  • 便于之后和ProdService的切换

我们声明一个字段,并在构造函数中实例化:

public class ArticleController {
	private IArticleService service;
	public ArticleController() {
		service = new ArticleService();

PS:为了代码文件夹的条理性,我把所有Service层的项目放到了SRV文件夹下:


model绑定

Hander method中的model参数,除了可以响应时向模板“传递”数据,也可以在接收HTTP请求时,将“绑定(bind)”请求中的数据。

文本型

<label>用户名:</label> <input type="text" name="username" /><br />
<label>密码:</label> <input type="password" name="password" /><br /> 
<label>自我介绍:</label><br />
<textarea name="selfDescription"></textarea><br /> 

不要忘了还有隐藏文本域:

<input type="hidden" value="fg" name="teacher" />

对应字符串

public class RegisterModel {
	private String username;
	private String password;
	private String selfDescription;
	private String teacher;

演示:控制台输出:

@RequestMapping
public ModelAndView input(RegisterModel model) {
	System.out.println(model.getUsername());
	System.out.println(model.getPassword());
	System.out.println(model.getSelfDescription());
	System.out.println(model.getTeacher());

选择型

比如:radio、checkbox、select等表单元素:

<label>性别:</label> 
<label>
	<input type="radio" name="gender" value="0" checked /> 男</label> 
	<input type="radio" name="gender" value="1" />女 <br /> 
<label>

<label>年龄:</label> 
<select name="age">
	<option value="1">one</option>
	<option value="2" selected>two</option>
	<option value="3">three</option>
</select><br />

<label>爱好:</label>
<label> <input type="checkbox" name="hobbies" value="table-tenis" />乒乓球</label> 
<label> <input type="checkbox" name="hobbies" value="chess" />象棋</label> 
<label> <input type="checkbox" name="hobbies" value="LOL" />LOL</label>
SpringMVC会在绑定时自动的进行类型转换,比如上述表单元素,对应的Model就可以是:
public class RegisterModel {
	private Boolean gender;
	private Integer age;
	private String[] hobbies;
演示:控制台输出
System.out.println(model.isGender());
System.out.println(model.getAge());
for (int i = 0; i < model.getHobby().length; i++) {
	System.out.println(model.getHobby()[i]);
}

这种类型转换是比较灵活的,比如:

  • gender的value既可以是1和0,也可以是true和false,他们都可以被自动转换成Boolean值
  • hobbies既可以是String[]数组类型,也可以是List<String>集合类型

引入:GET和POST Handler的区分

@RequestMapping(method = RequestMethod.GET)
public ModelAndView input() {

@RequestMapping(method = RequestMethod.POST)
public ModelAndView input(RegisterModel model) {


Freemarker表单标签库

实际开发中,我们通常使用更高效的标签库(tag lib),比如freemarker中可以这样写:

<#import "spring.ftl" as spring>

<form method="post">
	<@spring.formInput "command.age"/>

但无论如何,我们首先要明白:所有这些标签库最终都是为了解析成和Model相呼应的HTML标签。其核心问题是获得Model的字段名……

spring.ftl

演示:在spring-webmvc-xx.jar中,包org.springframework.web.servlet.view.freemarker下有一个内置的spring.ftl文件

spring.ftl中定义了大量的宏(macro),在我们的form表单页直接引入(import)这个文件,

<#import "spring.ftl" as spring>

总是建议使用标准的spring命名空间(别名)。

然后就可以使用这个宏里面的内容了。

PS:根据版本不同,可能需要在springmvc-servlet.xml的FreeMarkerViewResolver节点下添加

<property name="exposeSpringMacroHelpers" value="true"/>

显式的暴露这个spring.ftl宏

<@spring.formInput "command.age">就开始调用formInput了,传递的参数被称之为path,前缀

command

实际上是handler method中添加到model中的一个键名,存储的是一个POJO对象:
  • 键名command是(为了和JSP中的form标签呼应)约定俗成的
  • 值就是要传递给form表单使用的model

public ModelAndView input(Model model) {
	model.addAttribute("command", new RegisterModel());
或者:
public ModelAndView input(@ModelAttribute("command")RegisterModel register,   Model model) {

而command.后面对应的是model的字段名。

演示因为不匹配而报运行时错误:

  • model.addAttribute()中键名为comman
  • <@spring.formInput "command. 后接不存在的字段名,比如aged

背后原理

<#macro formInput path attributes="" fieldType="text">
    <@bind path/>
    <input type="${fieldType}" id="${status.expression?replace('[','')?replace(']','')}" 
name="${status.expression}" value="<#if fieldType!="password">${stringStatusValue}</#if>" ${attributes?no_esc}<@closeTag/>
</#macro>

其中,status来源于@bind,

<#macro bind path>
    <#if htmlEscape?exists>
        <#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
而@bind中最有价值的是:springMacroRequestContext

这是SpirngWeb为没有内置的request等对象的模板(比如FreeMarker),专门准备的一个RequestContext对象。

演示:

  • org.springframework.web.servlet.view.AbstractTemplateView中
    public static final String SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE = "springMacroRequestContext";
  • 将一个org.springframework.web.servlet.support.RequestContext对象存入model
    model.put(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE,
    		new RequestContext(request, response, getServletContext(), model));

在freemarker模板中,直接使用springMacroRequestContext

${springMacroRequestContext.getQueryString()}

而getBindStatus()方法就根据path取相应model对象字段的:

  • :status.expression
  • :status.value(演示给model默认值)
生成相应的HTML form表单元素。


其他参数和标签

formTextarea 和 formInput极其类似,path后面可以再接一个attributes参数,
<@spring.formInput "command.age" 
    "style='background-color: blue;' disabled" 
    "password" />
但formInput独有的第三个参数是fieldType,可以指定其type
<@spring.formTextarea 
    "command.username" 
    "" />
另外5个是选择型表单标签:
  • formCheckbox:单个复选框
  • formSingleSelect:单选下拉列表
  • formMultiSelect:多选下拉列表
  • formRadioButtons:单选按钮列表
  • formCheckboxes:多选按钮列表

formCheckbox(注意单复数)是单个的复选框:

<@spring.formCheckbox "command.gender" />
传入的path一般来说都是Boolean值,有趣的是生成的HTML元素:
<input type="hidden" name="_gender" value="on"/>
<input type="checkbox" id="gender" name="gender" />
@想一想@:这是为什么?

其他4个标签,除了path参数以外,他们还需要options,比如command.hobbies:

<@spring.formSingleSelect "command.hobby" 
	command.hobbies />
options是键为String类型的Map(Map<String, T>),一般都由model传递:
RegisterModel register = new RegisterModel();
Map<String, String> hobbies = new HashMap<>();
hobbies.put("1", "乒乓球");
hobbies.put("2", "读书");
hobbies.put("3", "LOL");
register.setHobbies(hobbies);
model.addAttribute("command", register);

Map中的String用于POST(给value赋值),值用于HTML呈现

<select id="hobby" name="hobby" >
            <option value="1" >乒乓球</option>
            <option value="2" >读书</option>
            <option value="3" >LOL</option>
</select>

formRadioButtons和formCheckboxes还:

  • 必须有第三个可选参数separator,用以隔离生成的<input>和<lable>标签,一般不建议使用(间距换行等应用CSS调节),传入空格即可;
  • 可以有第四个可选参数仍然是attributes,
	<@spring.formRadioButtons "command.hobby" 
		command.hobbies
		""  
		"class='radio-inline'"  />

生成的HTML如下所示:

    <input type="radio" id="hobby0" name="hobby" value="1"     >
    <label for="hobby0">乒乓球</label>
    <input type="radio" id="hobby1" name="hobby" value="2"     >
    <label for="hobby1">读书</label>
    <input type="radio" id="hobby2" name="hobby" value="3"     >
    <label for="hobby2">LOL</label>

注意:checkboxes后台绑定的最好是数组集合,否则就是逗号分割的字符串……


复杂类型绑定

model中的model

邀请人为例,在RegisterModel中再包含一个UserModel

public class RegisterModel {
	//避免null值报异常 
	private UserModel invitedBy = new UserModel();
然后我们就可以在freemarker中这样写:
<label>邀请人姓名:</label>
<@spring.formInput "command.invitedBy.username" /> 
演示:一样可以在后台实现model绑定
System.out.println(model.getInvitedBy().getUsername());
原理:生成的HTML代码name值为:invitedBy.username
<input type="text" id="invitedBy.username" name="invitedBy.username" value="">
SpringMVC会根据invitedBy.username自动查找model中的invitedBy属性,并为其(invitedBy属性对象)username赋值。

集合

之前讲的formRadioButtons和formCheckboxes并不能满足所有需求。

比如我的消息页面,checkboxes是彼此隔离的(或者说label是非常复杂的);且这时候model再用Map就不合适了,通常会使用List。


public class MineModel {
	private List<MessageModel> messages;
public class MessageModel {
	private int id;
	private String content;
	private LocalDateTime created;
通过MockService返回
public class MessageService {
	public MineModel Get() {
		MineModel model = new MineModel();
		
		List<MessageModel> messages = new ArrayList<>();	
		
		MessageModel m1 = new MessageModel();
		m1.setId(1);
		m1.setContent("飞哥好帅……");
		m1.setCreated(LocalDateTime.now());
		MessageModel m2 = new MessageModel();
		m2.setId(2);
		m2.setContent("飞哥好帅帅……");
		m2.setCreated(LocalDateTime.now());
		MessageModel m3 = new MessageModel();
		m3.setId(3);
		m3.setContent("飞哥好帅帅帅……");
		m3.setCreated(LocalDateTime.now());		
		messages.add(m1);
		messages.add(m2);
		messages.add(m3);	
		
		model.setMessages(messages);
		
		return model;
注意因为使用的是(注意单数),所以不能再像一样使用数组/集合啥的装用户选中的


handler method中检查model绑定:

for (MessageModel message : model.getMessages()) {
	System.out.println(message.getId() + ":" + message.getSelected());
}


想一想:为什么取不到id?


默认值

如果需要在页面加载时就设置一个默认的选中值,只需要在Controller中指定:

//"2"是(对应value的)键值
register.setHobby("2");

@DateTimeFormat

model中声明一个LocalDate字段enroll

private LocalDate enroll; 
在Controller中赋值
register.setEnroll(LocalDate.now());
对比演示:
  • 使用formInput
  • 直接model.enroll
<@spring.formInput "command.enroll"  /><br /> 
${command.enroll}
ʅ(‾◡◝)ʃ

解决办法:在enroll上添加注解

@DateTimeFormat(pattern = "yyyy-MM-dd")
pattern可以指定日期格式……


作业

  1. 利用model和MockService,使用freemarker,完成
    1. JSP章节作业的:
      1. 作业第2题:显示某月的日历
      2. 作业第4题:分页链接,包括
        • 上一页/下一页
        • 正确的总页数
      且能够用宏进行封装重用(使用springMacroRequestContext获取当前页面path等)
    2. Dispatch章节的:1、3-5题
  2. 利用Freemarker Tags Lib(文件上传除外)完成以下页面,
    1. 注册
    2. 登录
    3. 忘记密码
    4. 联系方式
    5. 用户资料
    6. 发布求助
    7. 发布文章
    8. 求督促
    并确保能在后台通过model绑定接收到用户输入。

补充:POST之后

按我们之前的代码,提交之后,进入POST的handler method,最后页面就会报错:

Caused by: java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'command' available as request attribute

@想一想@:为什么?因为没有给model添加command嘛!

#体会#:HTTP的无状态。

因为RegisterModel已经命名为model,不便再引入springframework.ui.Model,所以我们可以用注解的方式暂时解决这个问题:

public ModelAndView input(@ModelAttribute("command")RegisterModel model) {



Spring MVC Model
觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 Java Web开发 中继续学习:

我们的 特色

  • 面向就业
  • 线上线下结合
  • 同学互助
  • 师生共赢

报班 QQ群:273534701

  • 获取视频资料
  • 要求作业点评
  • 参加阶段性测试
  • 模拟面试