java web开发框架功能点整理

完成一个crud类java项目的框架最小功能集

后端部分

MVC

基于Web的项目,MVC应该是其核心功能了。有以下问题需要解决:

  • 路由:
    解决什么样的请求交给哪个后端处理单元处理的问题。
  • data binding:
    解决把前端传来的数据(url path、字符串形式的键值对、Header中的信息、cookie中的信息等,必然是字符串)包装为Java可以使用的对象的问题。要实现的效果是,所有需求对于后端业务代码来讲都要是透明的,不要再有手动的转换过程。
    一般的MVC框架都有比较清晰完善的解决方案,但有些边角的需求,需要注意怎样实现更为简便。例如如想传入的信息是三个student,每个student都有name、id两个属相,那怎样才能在后端业务代码中直接得到一个List<Student>?
  • 模板引擎:
    解决怎样把后端的处理结果转换为html页面显示给前端的问题。
    首先解决转发给哪个模板,例如使用jsp作为模板引擎,后端处理单元就要做到可以控制跳转到哪个jsp文件。
    其次是数据显示,后端处理单元处理所得到的数据要转化为字符串然后放到html中的相应位置上。需要注意的事如何能透明的对相应数据做格式化显示,如所有日期类信息都以yyyy-MM-dd的格式显示,不需要在任何逻辑控制单元中做手动转换
  • Ajax/Json:
    越来越多的请求将以ajax的形式返回给前端。简单来说,需要解决的就是怎样把后端处理的结果透明的转换为Json数据的问题。MVC框架一般都有相应的解决方案,通过简单的声明就可以控制信息以Json形式返回。需要注意的是,Model如果不是纯粹的贫血模型,怎样控制每个字段(或get开头的函数)是否要转化为json发给前端。

 

DB

数据源管理

通过统一配置,方便的在程序的任何位置获取到数据源信息,以便于进行数据库操作。

事务管理

通过约定(每次http请求作为一次事务)或者声明式的方式进行事务管理,对逻辑代码透明。

ORM

Java Web世界中大部分的数据库操作代码都是针对某个Java模型的Crud,需要提供相应的方案进行操作。至少包括:

  • 主键查询
  • 条件查询(分页)
  • 插入
  • 通过主键更新
  • 通过主键删除

必要情况下,应支持对逻辑代码透明的插入、更新、删除时间记录,以及假删除操作。

batch

某段特定sql的执行,如初始化数据等。需要提供相应的工具类。

list

一般的项目都有无穷多的显示数据列表的功能,如果针对每个列表再去开发,工作量较大。最好用配置的方式统一支持。

框架级别功能

权限管理

对逻辑功能透明、操作级。

提供登录用户管理、操作鉴权等基本机制。

提供功能模块管理、用户管理、角色管理、角色权限管理等基本功能。

杂项工具

log

提供逻辑与物理相分离的日志记录工具。

DataUtils/StringUtils/……

项目中的各种Utils,一般找合适的开源库都能解决,没有必要重新发明一遍轮子。

前端部分

UI

基本框架

统一风格、字体字号、主页面结构(项目标题、logo)等。

页面载入中效果

UI部件

  • alert、confirm、prompt……、
  • 模态窗口(显示少量信息和显示完整页面
  • form控件:下拉列表、日期控件等……
  • 消息显示(浮动出现自动消失、固定可手动关闭、固定不可手动关闭等)
  • ……

登录页

  • 用户名密码输入
  • 子系统选择
  • 登录成功后跳转
  • 登录失败后的提示、重新输入

通用功能

  • 个人profile
  • 系统设置
  • 皮肤更换
  • ……

列表页

  • 列表显示数据
  • 查询
  • ……

表单页

  • 简单表单布局
  • 复杂表单布局
  • 各种表单控件在各种布局下的展示

信息展示页

文字与图片混合排版等

统计图表页

各类统计图表的展现与交互

 

 

 

 

Java Web开发教程——基本思路

思路:

将需求拆解为基本技术的简单组合,再分别实现。

  • 基本技能必须是足够简单且容易理解的。
  • 对基本技能的组合,必须是严格相等于需求域的,不能有缺漏。

一、基本技能

基础知识

B/S(Browser/Server),就是以浏览器作为用户界面与用户进行交互,逻辑在服务器中进行处理的项目架构

所以B/S项目的基本形式就是:

  • 用户通过浏览器向服务器请求资源或请求操作
  • 服务器操作后,将用户想看到的信息交给浏览器,浏览器将这些信息显示出来,并提供用户下一步操作的入口

Http基本流程

与C/S开发不同,B/S项目开发严格遵守“请求-》响应”的循环,任何一个需求的实现,都要拆分成若干个“请求-》响应”的流程组合。

每次“请求-》响应”的基本流程如下:

  • 前端请求-》Http传输-》后端路由-》后端处理-》响应-》前端显示

前端请求怎样发送

Http的请求,在简单项目应用中,可视作包含以下几部分:

  • url:用于服务器定位由哪个逻辑单元处理这个请求
  • 方法:GET、POST等。在简单的Java Web开发技术中,这二者没有特别严格的区分,但考虑到HTTP的语义,可大致区分为:对服务器无影响的操作,用GET;有影响的(如添加或修改了数据)用POST
  • 参数:包括参数名和参数值。每个参数名称可以对应多个值

先将需求的请求提交部分抽象为以上几部分,即url是什么、用什么方法提交、包括什么参数。再用浏览器标准支持的形式实现。

浏览器提交请求的方式可包括以下几种:

  • 在地址栏输入url并回车提交。此时提交的是GET请求。
  • 通过提交表单(form)的形式提交。此时,根据form的method属性决定提交方法,默认为GET。提交表单可以通过点击表单内type=submit的按钮或input,也可以通过js控制提交。
  • 点击超链接,做页面跳转,相当于在浏览器中输入地址。因此提交的也是GET请求。
  • 通过js控制做页面跳转,有各种各样的方式。

GET请求的参数将包含在url中,格式为

xxx.xx?param1=value1&param1=value2&param3=value3

表示将请求提交到xxx.xx,包含名称为param1的两个参数值和名称为param3的一个参数值。

Http传输

Http传输是通常俗称的前端和后端的分界线。

浏览器负责提交Http请求,通过网络设备,将该请求提交给服务器。此步骤一般不需要自行写程序实现。

可通过浏览器调试工具,查看Http传输的内容。如Chrome点击F12打开的开发工具,或Firefox下的firebugs。

对Http程序的调试,应优先关注传输部分。如果程序运行有误,先通过浏览器调试工具确定提交的请求是否与你预期的相符。如果相符,则基本确定问题出现在后端,如果不相符,则基本确定问题出现在前端。

后端路由

在项目采用的技术范畴下,可将服务器接收到Http请求开始,直到将此请求传递给为了完成该请求而写的逻辑处理单元(如Servlet、Struts的Action、SpringMvc的Controller等)为止,这其中的部分视作“路由”部分。

这部分主要解决的问题是,怎样根据Http请求的内容,将该请求传递给正确的逻辑处理单元。

以Java Web项目为例:

请求发送到服务器后,根据请求url的端口决定交由哪个程序处理,一般会由tomcat或者netty等Java Web服务器监听该端口。根据请求内容,Java Web服务器会根据web.xml的配置,决定由哪些Servlet、Filter等参与处理,一般会将请求配置分发给Struts、Springmvc等框架。

框架会根据请求url中的内容,以及符合该框架要求的配置信息,将请求分发给某个具体实现的Java类。此类为项目开发人员编写的,一般从这个步骤开始,程序的控制权开始由项目开发人员的代码控制。配置方式要解决的问题,就是http请求与Java类的映射逻辑,比如“以.action结尾的并且.前面的部分是login的请求,与LoginAction匹配”。

后端处理——数据绑定部分

在控制权转移到项目开发人员自行编写的特定代码后,仍需要解决如何将http请求中的数据包装为这些代码可以利用的形式的问题。

例如通过Java Web进行开发,必须解决前端传入的param1=value1这些参数,如何在Java程序中使用的问题。

不同的框架有不同的绑定方式,如Struts2的方式是在Action类中找到与参数名称对应的get函数,如请求中有param1=value1,则框架会在该Action程序执行之前,执行setParam1(value1),那么我们可以在这个函数中将value1赋值给某个我们之后可以利用的变量,就可以在我们自己的代码中使用了。

后端处理——逻辑部分

将前端的参数接收到之后,就可以使用当前语言提供的方式,去做特定的逻辑处理。只要参数接收正确,那么这部分的处理与一般的简单Java程序没有区别。

存取数据库

业务类系统的重要部分就是对数据的持久化操作,大部分情况下都利用数据库实现这一需求。

一般存取数据库的基本方法都会在框架层面解决,在开发之前需要了解这些基本方法,需要时直接调用即可。

Java开发一般采用比较严格的Object-Relation-Mapping模式,即每张数据库表都会有一个Java类对应。大部分情况下,此Java类就是标准的Java Bean。因此,这个步骤需要具备的技术基础,就是要解决“给定一个Java Bean,怎样保存到数据库中,怎样更新到数据库中……”这一类的问题

后端处理——视图部分

后端的逻辑处理完成后,大部分情况会生成一个用户界面,反馈给用户。具体到B/S项目开发,就是要生成Html页面以及配套的js、css等内容。

js用于控制前端的逻辑,css用于控制前端样式,这两者一般都是静态的内容,大部分情况下也不会包含在需要逻辑处理的请求中,而是通过html中的script、link等标签,让浏览器另外发送一个资源请求去获取这些文件。

页面显示的内容,通过html表示。而在逻辑处理完成后,通常情况下都会根据逻辑处理的结果,去动态生成html内容。用Java代码生成html内容的方式,将类似于:

[java]

print("&lt;html&gt;");<br data-mce-bogus="1">

print("&lt;body&gt;");<br data-mce-bogus="1">

print("&lt;h1&gt;" + title + "&lt;/h1&gt;");<br data-mce-bogus="1">

[/java]

 

非常繁琐。

因此,采用jsp的形式完成以上内容。

jsp是一种语法类似于html,但支持动态内容生成语法的语言。servlet容器将会把jsp处理成html响应给前端。

在这部分我们需要解决的问题包括:

  1. 上一步的逻辑处理单元处理完成后,将控制流程转移给哪个jsp。例如struts2中,将会通过配置view的方式,将Action函数的返回值与某个jsp文件对应。在springmvc中,可以直接返回jsp路径,也可以通过其它方式配置更复杂的对应关系。
  2. 怎样控制jsp生成html。jsp中类似于html的部分,将会直接生成同样的html。包含动态语法的部分,将会根据规则,计算出应该生成的html。这个计算过程,一般会用到上一步逻辑处理单元的逻辑处理结果。例如

    逻辑处理单元中,根据前台传入的参数,去数据库中读取了一个表示人员信息的对象,在jsp中就有对应的方法去获取这个对象的各项值,并使用。使用的方式最常见的是el表达式,另外也包含jsp标签等内容。

生成html后,容器将负责将此内容响应给客户端浏览器。

另外,也会有一些不生成html内容的流程,如最近比较流行的restful api方案,就会返回一些特定格式的json。但其原理和基本流程是不变的,都是后端通过各种方式,生成各种格式的“字符串”。这些字符串都符合前端使用者(如浏览器)的某些规范,因此可以控制前端使用者的行为。

响应

容器负责将处理结果以http相应的方式发送给前端。此相应一定是和之前的请求对应的。此部分一般也不需要自己写代码负责。

前端显示

如果是浏览器负责前端的显示和交互,则一般会运行html、js、css等几类的文件。而这些文件,是后端生成后响应给前端的。因此,后端通过上面提到的各种方式,控制这几类文件生成后的内容,再将这些内容交给前端,从而控制前端的行为。

前面提到过,在实际开发中,js、css一般都是静态文件,动态生成的部分是html。动态生成的技术主要是jsp。在编写jsp的过程中,要能分析出各种情况下生成的html是什么样子的,从而指导jsp文件的编写。

在了解以上流程后,此部分需要解决的问题就是,什么样的html、js、css,会在浏览器上显示出什么样的内容。

Html为“标记性语言”,基本语法为“标签+属性”。因此,编写html的基本思路为:什么样的需求对应什么样的标签,然后用什么样的属性去控制细节。一定不要背诵所有标签的含义,而是要理解这个思路,并且在用的时候查询即可。

js是一门程序设计语言,与java类似,初期可以互相参考理解。但js的程序设计范式与java不同,因此在后期深入时,要注意与类java语言的区别。另外,js初期的主要应用目的是改变html的内容,这部分主要对应的知识是“js的dom操作”,即“通过什么样的js可以改变某个标签的某项内容”。

前端开发的复杂之处在于,任何一项需求都有很多方式与之对应。而选择的方案如果不当,会在很久以后才会显出弊端,增加了调整的成本。我认为比较重点的原则之一是“语义化前端开发”,即语言要与含义对应,例如想显示一个表格,用table标签也可,用若干个div控制样式也可,但从语义角度讲,我们要一个表格,就一定要用table标签。

 

案例分析:完成登录功能(待补充)

一、了解需求

能做到完全表述需求的所有流程。是否做到“完全表述”,取决于表述内的所有词汇,是否都属于某个在工程范畴内已经被良好定义的“技能表”或者“词汇表”。如果不能做到,则要不断精化。

例,第一版需求:

输入用户名和密码登录

需要考虑,输入、用户名、密码、登录,这几个词汇,是否都是已经被良好定义的。之所以要强调在“工程”范畴内,是因为我们并不是在做汉语研究,没有必要追溯每个词语的最终源头,而只是在当前的工程知识范围内解释解释即可。如果是良好合作的团队,则以上几个词在要求不太严谨的情况下都不用解释。但如果是新人新团队,则需要在初期建立一定的共识。

输入:这是一个用户的动作。由于做的是web项目,所有的用户界面都由浏览器端完成,其主要实现方式是html+js+css。由于html是标准化很完善的技术,”输入“对应的是哪个标签,应该是技术上的共识,因此不必过多解释。既然是用户界面,那么一定涉及到用户体验设计。由于项目类型是”后台系统“,这一项要求也不是十分严格,采用项目成型的样式库即可。

用户名、密码:具体含义用常识理解即可。这两项

二、形成设计方案

附录:参考知识

  • Http原理
  • Html基本使用方法

Web开发教程——命令行入门

初学Java的人一般都会被如何设置path环境变量折腾一顿,到了学JavaWeb的时候,又会被CATALINA_HOME之类的环境变量再折腾一顿。了解了命令行运行程序的原理之后,就可以脱离见招拆招乱试的困境。

基础知识

  •  计算机世界中,我们写的、我们用的,都是“程序”。程序需要一定的方式启动,启动的时候,可以传递参数。
  • 双击桌面上的程序图标、点击开始菜单中的程序图标、双击某个类型的文件(如.doc)后操作系统自动用某个程序(如Word)打开,这些都是图形化Windows提供给我们的,运行程序的方法

命令行概念

操作系统会根据自身特点,提供人机交互工具,Windows的图形化界面就是一种。大部分的操作系统都会提供命令行形式的人机交互工具。

命令提示符(运行-》cmd)就是Windows下的命令行工具。大部分Linux,进去之后就直接是个shell环境了,这就是Linux下的命令行工具。

命令行,顾名思义,输入命令、换行,系统按照你的命令执行。这些所谓的命令,其实都属于上面说的程序。

图形化界面易用性好,但由于操作方式基于二维平面上的鼠标点击,想把某个操作录制成脚本,自动执行,很困难。而命令行,由于是纯文本输入输出,利于制作脚本,随便打开个纯文本编辑器,每条命令输入进去,保存,就可以了。因此,命令行是自动化执行任务的主要途径

命令行基本语法

输入命令或程序,补充一些参数,回车执行。

命令和程序的区别,暂时没有找到太严谨的定义。我的理解是,操作系统提供命令行环境本身的一些工具,如cd,属于命令。对于这些命令,并没有一个单独的程序叫cd的与之对应。而程序,应该是符合操作系统规则的独立可执行文件。

例:dir /p
  • 第一个单词,是命令或者程序名称,如上面的dir
  • 第一个单词以后的部分,是参数,如上面的/p。某些参数用于配置程序的运行方式,如dir /p用于配置分页显示。某些参数是作为路径,指定了一个文件给该程序使用。具体的参数含义,取决于该程序怎样使用,在操作系统和命令行工具层面,并没有特殊的含义。
  • >用于组合一个命令(以及参数)和一个文件,会将前一个命令在控制台输出的内容 输入到文件中
  • |用于组合两个命令(以及参数),会将前一个命令在控制台输出的内容输入给第二个命令

Path问题

输入什么能被正常执行?

命令的处理方式是特殊的,这里只说程序。

将输入的内容作为文件路径,找文件,找得到的话,如果该文件是可执行的,则执行,这样,程序就运行起来了。

所以,我们需要解决的是,输入什么样的内容,可以让我们想要的那个可执行文件被找到。

另外,在输入表示文件路径的参数的时候,也要保证该文件可以被找到,从而程序才能使用这个文件。一般来讲,程序使用文件的方式也遵循操作系统找文件的一般习惯,因此,与找可执行文件的方式相同。

那么,什么样的路径能够被找到?

路径分为绝对路径和相对路径两种。绝对路径以/开头,表示从硬盘的根目录开始找起(Windows下,如果不在一个分区内,则通过盘符区分,如c:/abc……)。相对路径不以“/”开头,其“相对”指的是当前目录,即从当前目录开始,根据相对路径的内容找起。

任何时刻,命令行都有“当前目录”的概念。windows命令行工具中,会在行首提示当前目录,如:

D:\var>

可以通过cd命令切换当前目录。linux环境下的显示和操作方式也类似。

如果输入的是绝对路径,那么与当前目录无关,直接按照路径内容找就可以了。

如果输入的是相对路径,则以当前目录为基础,加上相对路径,进行查找。如当前的目录为d:/abc,输入的内容为bcd/ef.exe,则最终找到的内容为d:/abc/bcd/ef.exe。

相对路径中有两个特殊符号,一个点(.)表示当前目录,两个点(..)表示上级目录。如当前目录为d:/abc,输入的内容为../ef.exe,则找到的内容为d:/ef.exe。

环境变量

环境变量是指,在操作系统环境下命令行可以读取的,统一预定义的变量内容。在命令行下可以通过set命令查看和配置。

Window下有个特殊的环境变量,叫path,配置了多个目录,用“;”分隔。如果在命令行运行程序时输入的是不包含路径的程序名称,那么查找过程也会包含path中的所有路径。在查找顺序上,Windows貌似是先当前目录后path,而linux相反。

案例分析:永远找不到的javac

我刚学习java的时候,经历过一下午的各种找不到。貌似现在的jdk已经能在安装后让你直接运行了,如果是这样的话,那就换成永远都找不到的ant,永远都找不到的mvn,都可以。

当你输入javac并回车时,操作系统要怎样找这个程序?在当前路径中找,找不到的话就去path中找。如果都没有,那当然会提示找不到。

网上一定给出了很多教你如何设置path的方法,根据上面说的内容,应该可以知道为什么这样设置就能成功执行了。那再加个题目,能不能找到除了设置path外的第二种方法?用绝对路径就可以了。

那再加个一个题目,装完了oracle之后,eclipse就启动不起来了,是什么原因?

答案:eclipse要javac才能启动-》找javac的时候,依赖了path变量中的内容-》安装oracle时,oracle附带装了一个低版本的jdk,并且放到了path中的最靠前位置-》eclipse不能用这个低版本的jdk启动,于是报错了。

其它

本文中的不严谨

各种定义,措辞,基本上都是我随口说的,并没有经过严格考证。比如所有的内容,都是在我的当前操作系统,当前程序设计环境,当前的业务需求下想到,换成类linux环境,/和\的区别就能让这里的例子基本失效。

不要背诵本文中的任何结论,我想表达的是在我得出这些结论时的思考过程。

 

 

servlet在同一域下jsession冲突问题现象记录

之前知道同一个ip或者域名下,会有session相互覆盖的问题,原因是cookie中的jsession会互相覆盖,可以通过在tomcat中修改jsessionid的cookie名称的方式解决。

今天在研究单点登录的时候突然想到,tomcat的webapps文件夹下可以随便扔war包,也就是说,tomcat是支持在同一个host下部署多个webapp程序的,这显然是在一个域中。根据经验,这样是完全可行的,因此与之前的结论有矛盾。

新建了个javaweb项目,名字叫webapp,放了一个session listener,在session create的时候打印一句日志。然后把这个项目复制了一份,名字改成webapp2。两个项目同时启动。

通过浏览器访问webapp,控制台中输出了开始session的信息。访问webapp2,控制台中也输出了开始session2的信息。按照我的预计,由于是同域,此时webapp的jsessionid cookie应该已经被覆盖了,再访问webapp应该开始一个新session。在浏览器中试了一下,发现控制台中没有打印信息。两个webapp随意交错刷新页面,都没有导致新建session。说明这个流程下,没有发生cookie互相覆盖的问题。

通过chrome查看cookie内容也印证了这一点,确实有两个不同的jsessionid,而且分别保持不变。跟之前得出的结论不符。

再仔细看了一下,发现chrome中的cookie显示界面中还有path这一项,说不行cookie不仅仅是跟域相关,还跟path相关。

于是把两个项目分别放在监听不同端口的两个tomcat中运行,但是项目的文件夹名字都改成webapp,再刷新,就开始发生cookie了。

具体现象是:

访问webapp页面,webapp启动一个session,cookie中增加一个jsessionid。

访问webapp2页面,webapp2启动一个session,cookie中增加jsessionid,由于在同一个域下,将webapp中的相应内容覆盖了。

访问webapp页面,此时浏览器提交的是webapp2的jsessionid,服务端找不到对应的session,于是又创建了session,重新在cookie中存放了jsessionid。

访问webapp2页面,类似于上一步,又创建了新的session。

大致结论:

同域的相同名称的webapp会共享cookie,不论是否在同一个端口下。(当然如果是两个不同的webapp,一定不在同一个端口下)

同域的不同名称的webapp不会共享cookie,不论是否在同一个端口下。

即:

192.168.0.100:8080/webapp1与192.168.0.100:8080/webapp2不共享jsessionid。

192.168.0.100:8080/webapp与192.168.0.100:8080/webapp共享jsessionid。

 

 

非结构化操作日志解决方案草稿

需求:

记录系统的操作日志,要求可以做简单的查看详情功能。例如:

xx于xx年xx月xx日将xx人员信息修改为xxx

根据这条日志的内容,需要显示若干链接,点击之后可以查看详情。如上述日志内容与操作用户、被修改的人员信息相关,就需要在显示日志的时候提供查看操作人的信息、查看被修改人员信息的功能。

问题:

大部分操作日志是非结构化的,之前的方案是根据系统的核心功能,定义若干外键,强制转换为结构化的数据。如在日志表中增加人员id、单位id、用户id等若干字段,点击之后查看详情,这样的灵活性不高。

解决方案:

灵感来自于wordpress的操作日志:

image

图中共支持三种详情操作,即查看用户详情,查看类别强行和查看标签详情。这些链接都是嵌入字符串中的,不是在文字最后增加若干“查看XX详情”的链接,用户体验较好。

故有以下解决方案:

在生成日志内容的时候,将需要增加链接的部分用类似于html标签的形式包起来。如:

[user id=1]xx[/user]于xx年xx月xx日将[employee id=2]xx[/employee]信息修改为xxx。

针对每种标签,写一个标签处理类,将标签内容转换为符合html标准的超链接。标签和标签处理类的对应关系可以写xml或者properties配置,支持扩展。标签可以灵活支持各种属性。

多页面提交,统一持久化解决方案

需求:

人员信息包括一张主表和若干张子表,点击某链接进入某人员信息详情页面后,可以针对主表和子表信息进行修改。所有的修改统一保存后,打包提交到上级审核(相当于做底层的持久化操作),审核通过后,进行实际修改操作,审核不通过时,保留原信息。

计划采用Command和Strategy模式,将修改操作抽象为若干Command,每个Command都有changeStrategy函数做实际的变更。在Command之上建立Transaction概念,用以表示一批的修改内容,要么同时生效,要么同时不生效。

问题:

每个保存操作会提交一个单独的http请求到后台,此时需要作Command的持久化。多个修改操作需要多次重复此过程,最终每个Command都与同一个Transaction相关联。因此,需要在多次http请求之间共享Transaction信息。

解决方案:

使用JSP的session机制进行信息共享。考虑到客户端的不可靠,不能依赖浏览器的onclose事件来判断用户操作是否完成。

在session中建立Map,key为人员id,value为Transaction对象。

进入人员主表信息页面之前,先根据该人员信息的id在上述map中查询,如果有Transaction对象则删除,之后建立新的Transaction对象。

在每个修改请求的处理函数中,根据人员id在session中查找到Transaction对象,新建command并与之关联,一并存入session中。

点击统一的保存后,根据人员id查找到Transaction对象,修改状态并作持久化,进入下一流程。

优点:

依赖于用户的明确操作,即“点击保存按钮”,来完成持久化动作,避免浏览器异常退出后,流程不能正常完成。