bupt-web作业四:使用WEB技术实现一个简易的联系人管理系统
Spring MVC基于注解器的开发模式
在Spring boot中,嵌入了Tomcat等Servlet容器,同时支持尽可能的自动配置,因此当创建的Spring boot项目添加了spring-boot-starter-web依赖时(用spring initializr创建时勾选了web),在启动后会对dispatchservlet进行自动配置。
控制器的意义
Controller实现类包含了对用户请求的处理逻辑,是用户请求和业务逻辑之间的“桥梁”。
Spring MVC支持基于注解的控制器,无需再配置文件中部署映射,不必扩展基类,也不需要实现特定的接口。极大简化了Web应用的开发。
常见的注解:
- @Controller,配置在类上,表明类是Spring MVC中的Controller,Spring MVC会自动扫描注解了此注解的类。
- @RequestMapping,可以配置在类和方法上,用来指定类或者方法是针对哪个URI的处理器。一旦声明时RequestMapping,无论时GET还是POST请求都可以被拦截下来
- @ResponseBody,表示将返回值放在response体内,而不是返回一个页面。
- @RestController,是一个组合注解,组合了@Controller和@ ResponseBody,当后端只返回数据时(如json),就可以使用此注解。
- @PathVariable,用来接收路径参数,此注解放置在参数前。
- @SessionAttribute:用在处理器的类上,用于在多个请求之间传递参数,类似于Session的Attribute,但不完全一样,一般来说@SessionAttribute设置的参数只用于暂时的传递,而不是长期的保存
- @ModelAttribute:
- 用于方法上时:通常用来在处理@RequestMapping之前,为请求绑定需要从后台查询的model;
- 用于参数上时: 用来通过名称对应,把相应名称的值绑定到注解的参数bean上;要绑定的值来源于:
- @SessionAttributes 启用的attribute 对象上;
- @ModelAttribute 用于方法上时指定的model对象;
- 上述两种情况都没有时,new一个需要绑定的bean对象,然后把request中按名称对应的方式把值绑定到bean中。
Spring工作流程
这里我只说下最最最常用的(题里用最多的)
controller:
总体来说,每个请求都会被Dispatcher Servlet获取(是的,一个单纯的Servlet工程其实很冗杂,而且单纯的Servlet工程效率并不高),之后会根据拦截到的url指派一些注解器服务。
一个Controller一般会以一个模型(Model)作为返回对象,所谓模型其实就可以理解为一个“对象”,比如一个变量啊,或者自定义的一个Java类啊都算是一个模型。模型返回给Dispatcher后一般就会交付给一个View进行处理,所谓View其实就可以理解成前端的东西,例如一个html文件。在这一步中,模型会被“渲染”,即通过前端接收用户自己的一些操作从而改写模型的一些数据,进而用户可以请求更多的服务。
一般情况下我们可以认为一个Spring工程时默认有一个Dispatcher Servlet的,各个注解器的拦截安排就算是Dispatcher Servlet的工作。
Thymeleaf
Thymeleaf :Java服务端的模板引擎,不新增标签,采用拓展属性(th:xx)去跟服务端进行数据交互,保留原始页面风格,使用浏览器直接打开,相当于打开原生页面,简洁漂亮、容易理解,完美支持HTML5,给前端人员也带来一定的便利。
Thymeleaf基础语法
基于Thymeleaf新建Spring项目
点击新建,选择Spring Initializer
等一下网络连接,之后包和Artifact随便起个名字就行
至少选择Spring Web和Thymeleaf
之后开始等待。。。系统会动态给你选择一个Maven版本并自动安装
这里需要注意一个技巧,即镜像的配置。因为IDEA毕竟不是国内的软件,Spring Boot也一样,所以下载很多东西其实都是在访问外网,有时候会不经意间卡死。所以,可以借助国内一些很棒的镜像做这件事,以更快配置好我们的工程环境。
- 小伙伴们可以以此为参考找一下该目录下的settings.xml
- 打开settings.xml,在mirrors那一段标签内容里加上
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>https://maven.aliyun.com/repository/public/</url>
</mirror>
这样配置的话最好配好后重启一下IDEA,之后的下载会顺畅很多。
编写工程
工程结构
根据题目要求以及关于Spring Boot基本知识,先暂时定一个工程框架出来:
那么同时我也决定将本工程的核心设置在对Controller和其Service的管理上。
总体思路如下:
登录服务
关于登录服务,基本的思路就是:
- 设置一个表单类,可以用来记录用户名、密码
- url访问login操作时会被捕获,并触发login相关的Service。在该Service中,会以该Java类作为Model交给前端处理
- 前端渲染的方式可以利用Tymeleaf将输入信息和类绑定
LoginController
提供的服务如上述。该控制器注解器核心任务是:
- 拦截一些GET请求,在用户未登录时返回登录界面以及一个可修改的Model;已登陆时直接重定向到联系人列表
- 拦截login.html内的form表单以POST提交的数据(Model),如果用户名和密码符合规范则予以通过,进入联系人列表;否则,设置错误信息,将当前Model重新交由login.html渲染
//引用的类和包略
@Controller
public class LoginController {
//直接访问/login或直接进入8080端口
@RequestMapping({"/","/login"})
public String login(LoginInfo user, Model model, HttpServletRequest request){
Object flag = request.getSession().getAttribute("login");
//未登录,交给login.html渲染
if(null == flag){
//准备一个模型的数据,将模型的数据加载回login.html中
flag=model.getAttribute("user");
if(null == flag){
model.addAttribute("user",user);
}
//返回login.html进行渲染
return "login";
}
//已经登录,直接重定向到通讯录列表
else
return "redirect:/list";
}
//登录后的检测,通过login.html的 form action以post方式访问
@PostMapping("/checklogin")
public String checkLogin(LoginInfo user, Model model, HttpServletRequest request){
//用户名密码正确,设置session中的login参数,并进入联系人表格界面
if(user.getUsername().equals("admin") && user.getPassword().equals("123456")){
user.setMessage("您已登录");
request.getSession().setAttribute("login",1);
return "redirect:/list";
}
//用户名或密码错误,需要重新返回登录界面,但是为了有提示信息,所以直接保留当前user,通过该结构内的message设置提示信息
else{
user.setMessage("用户名或密码错误!");
user.setUsername("");
user.setPassword("");
return login(user,model,request);
}
}
//直接从网址走到这访问,如果没登录要重定向回login,如果已经登录可以直接进入列表位
@GetMapping("/checklogin")
public String redirectLogin(HttpServletRequest request){
if(null == request.getSession().getAttribute("login")){
return "redirect:/login";
}
else{
return "redirect:/list";
}
}
//直接进入联系人列表的请求
@GetMapping("/list")
public String showMain(HttpServletRequest request){
Object flag = request.getSession().getAttribute("login");
if(null != flag){
HttpSession session = request.getSession();
//如果访问时还是一个空列表对象,就先为其分配一个空间
if(null == session.getAttribute("table")){
Table table = new Table();
session.setAttribute("table",table);
}
return "list";
}
else
return "redirect:/login";
}
//退出请求
@RequestMapping("/exit")
public String exit(HttpServletRequest request){
request.getSession().removeAttribute("login");
return "redirect:/login";
}
}
LoginInfo登陆界面消息 Java类
这是一个基本的Java类,其中包含了登陆界面涉及到的用户名、密码。同时还设置了一个message变量,用于协助在login.html内显示登陆错误信息。
package myspring.struct;
import java.io.Serializable;
public class LoginInfo implements Serializable {
private String username,password,message;
public String getUsername(){
return username;
}
public String getPassword(){
return password;
}
public String getMessage(){
return message;
}
public void setMessage(String message) {
this.message = message;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
}
login.html
这就是登录界面前端,它将对传过来的模型进行渲染,即:用户在框内输入,点击提交的同时也会设置模型(LoginInfo)中的username、password,从而使得Controller可以针对模型进行一些用户校验
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!--引用样式文件,位置随意-->
<link rel="stylesheet" href="../static/login.css"/>
</head>
<body>
<!--应要求,在页面开头提示需要输入的用户名密码-->
<p>
用户名->admin
密码->123456
</p>
<!--form表单提交的用户名和密码会作为一个结构体LoginInfo提交,该结构体包含username和password-->
<!--传给login.html进行处理的模型是一个LoginInfo类型的变量user-->
<form th:object="${user}" action="checklogin" method="post">
<!--field把input的内容绑定到一个变量中-->
<table>
<tr>
<td><span>用户名</span></td>
<td><input th:field="*{username}" th:text="${user.username}" type="text" id="username" required></td>
</tr>
<tr>
<td><span>密 码</span></td>
<td><input th:field="*{password}" th:text="${user.password}" type="password" id="password" pattern=".{6,}" maxlength="16" required></td>
</tr>
</table>
<input type="submit" value="登录">
<!--登录错误提示信息,与user的message变量绑定,如果message是空就不会显示这个标签-->
<span class="alert" th:if="${user.message}" th:text="${user.message}"></span>
</form>
</body>
</html>
联系人列表操作与相关服务
关于列表服务,就是增删改查。这里关于Thymeleaf也有一个核心的应用就是其可迭代对象
changeController
package myspring.demo;
import myspring.struct.Linkman;
import myspring.struct.Table;
import myspring.struct.changeTable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
//访问添加页面、判断修改状态页面,在Session中创建和add.html传递数据的类linkman
@Controller
public class changeController {
//通过导航栏直接访问
@GetMapping({"add","/checkadd","/change","/checkchange","/del"})
public String goBack(HttpServletRequest request){
Object flag = request.getSession().getAttribute("login");
if(null != flag){
HttpSession session = request.getSession();
if(null == session.getAttribute("table")){
Table table = new Table();
session.setAttribute("table",table);
}
return "redirect:/list";
}
else
return "redirect:/login";
}
//点击"修改"按钮
@PostMapping("/change")
public String showChange(@ModelAttribute(value="row")Integer row, HttpServletRequest request, Model model){
//修改会将当前列表行传过去
Table t = (Table)request.getSession().getAttribute("table");
//利用这一行列表实例化一个联系人对象
Linkman linkman = t.getTable().elementAt(row);
//将联系人对象传给change.html以便修改
model.addAttribute("linkman",linkman);
return "change";
}
//在change.html点击提交
@PostMapping("/checkchange")
public String checkChange(Linkman linkman, HttpServletRequest request){
Table t = (Table)request.getSession().getAttribute("table");
changeTable.changeElement(t,linkman);
return "redirect:/list";
}
}
linkman联系人 Java类
每个联系人包含姓名、电话、邮箱、住址、QQ等信息
package myspring.struct;
public class Linkman {
private String name,tel,email,address,QQ;
private String message;
public Linkman(String name,String tel,String email,String address,String QQ){
this.name = name;
this.tel = tel;
this.email = email;
this.address = address;
this.QQ = QQ;
}
public String getName(){ return name; }
public String getTel() {
return tel;
}
public String getEmail() {
return email;
}
public String getAddress() {
return address;
}
public String getQQ() {
return QQ;
}
public String getMessage(){ return message; }
public void setMessage(String message) { this.message = message; }
public void setName(String name){ this.name = name; }
}
Table 联系人数组 Java类
主要是列表,因为在list.html中,我们使用了thymeleaf的可迭代对象,这就是根据联系人列表展现出来的
package myspring.struct;
import java.io.Serializable;
import java.util.Vector;
public class Table implements Serializable {
private final Vector<Linkman> tableinfo;
//对应初始化列表
public Table(){
tableinfo = new Vector<Linkman>();
//可以选择对自己的列表联系人初始化一些联系人出来,也可以选择只是一个空表
// tableinfo.add(new Linkman(
// "怡宝的代言人连高波", "17333695199", "915799721@qq.com", "北京市海淀区北太平庄街道西土城路10号", "915799721"
// ));
}
//返回当前的列表全内容
public Vector<Linkman> getTable(){
return tableinfo;
}
}
list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>table</title>
<link rel="stylesheet" href="../static/list.css"/>
<script src="../static/list.js"></script>
</head>
<body>
<div class="content">
<table>
<thead>
<tr>
<td>姓名</td>
<td>电话</td>
<td>邮箱</td>
<td>住址</td>
<td>QQ</td>
</tr>
</thead>
<tbody>
<!--循环迭代对象Thymeleaf的th:each,通过传入的getTableInfo这个Vector可以将各个行展现出来-->
<tr th:each="m:${session.table.getTable()}">
<td th:text="${m.getName()}"></td>
<td th:text="${m.getTel()}"></td>
<td th:text="${m.getEmail()}"></td>
<td th:text="${m.getAddress()}"></td>
<td th:text="${m.getQQ()}"></td>
<td style="border: 0;">
<button class="changebutton" onclick="change(this)">编 辑</button>
<button class="delbutton" onclick="del(this)">删 除</button>
</td>
</tr>
</tbody>
</table>
<div class="change_div">
<button class="addbutton" onclick="add()">添 加</button>
<button class="exit" onclick="exit()">退出系统</button>
</div>
</div>
</body>
</html>
add.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"><!--自适应宽度和缩放-->
<title>Add</title>
<link rel="stylesheet" href="../static/add.css"/>
<script src="../static/add.js"></script>
</head>
<body>
<div>
<h1>新的联系人</h1>
<!--form表单提交的联系人会作为一个结构体linkman提交,包含姓名 电话 邮箱 地址 QQ-->
<form action="/checkadd" method="post" th:object="${linkman}">
<table>
<tr>
<td><span>姓名</span></td>
<td>
<input type="text" id="linker_Name" th:field="*{name}"
oninput="check('linker_Name')" pattern="[\u4e00-\u9fa5|a-zA-Z|\s*]{2,}"
placeholder="请输入姓名" required/>
</td>
<td><p id="pname">请输入姓名</p></td>
</tr>
<tr>
<td><span>电话</span></td>
<td>
<input type="text" id="linker_Phone" th:field="*{tel}" maxlength="11"
oninput="check('linker_Phone')" pattern="1\d{10}"
placeholder="请输入8位手机号" required/>
</td>
<td><p id="pphone">请输入电话</p></td>
</tr>
<tr>
<td><span>邮箱</span></td>
<td>
<input type="email" id="linker_Email" th:field="*{email}"
oninput="check('linker_Email')" pattern="(\w-*\.*)+@(\w-?)+\.+(com|cn)+"
placeholder="请输入邮箱" required/>
</td>
<td><p id="pemail">请输入邮箱</p></td>
</tr>
<tr>
<td><span>住址</span></td>
<td>
<input type="text" id="linker_Address" th:field="*{address}" oninput="check('linker_Address')"
autocomplete="off" maxlength="50" pattern=".+"
placeholder="请输入住址" required/>
</td>
<td><p id="paddress">请输入住址</p></td>
</tr>
<tr>
<td><span>QQ</span></td>
<td>
<input type="text" id="linker_QQ" th:field="*{QQ}" oninput="check('linker_QQ')"
maxlength="10" pattern="[0-9]{8,10}"
placeholder="请输入QQ号" required/>
</td>
<td><p id="pqq">请输入QQ</p></td>
</tr>
</table>
<br>
<input type="submit" id="sub" value="提交"
style="margin: 0 auto;">
<span class="alert" th:if="${linkman.message}" th:text="${linkman.message}"></span>
</form>
</div>
</body>
</html>
change.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"><!--自适应宽度和缩放-->
<title>change</title>
<link rel="stylesheet" href="../static/add.css"/>
<script src="../static/add.js">
</script>
</head>
<body>
<div>
<h1>修改联系人</h1>
<form action="/checkchange" method="post" th:object="${linkman}">
<table>
<tr>
<td><span>姓名</span></td>
<td>
<input type="text" id="linker_Name" th:field="*{name}"
oninput="check('linker_Name')" pattern="[\u4e00-\u9fa5|a-zA-Z|\s*]{2,}"
placeholder="请输入姓名" required readonly/>
</td>
<td><p id="pname">姓名</p></td>
</tr>
<tr>
<td><span>电话</span></td>
<td>
<input type="text" id="linker_Phone" th:field="*{tel}" maxlength="11"
oninput="check('linker_Phone')" pattern="1\d{10}"
placeholder="请输入8位手机号" required/>
</td>
<td><p id="pphone">请输入电话</p></td>
</tr>
<tr>
<td><span>邮箱</span></td>
<td>
<input type="email" id="linker_Email" th:field="*{email}"
oninput="check('linker_Email')" pattern="(\w-*\.*)+@(\w-?)+\.+(com|cn)+"
placeholder="请输入邮箱" required/>
</td>
<td><p id="pemail">请输入邮箱</p></td>
</tr>
<tr>
<td><span>住址</span></td>
<td>
<input type="text" id="linker_Address" th:field="*{address}" oninput="check('linker_Address')"
autocomplete="off" maxlength="50" pattern=".+"
placeholder="请输入住址" required/>
</td>
<td><p id="paddress">请输入住址</p></td>
</tr>
<tr>
<td><span>QQ</span></td>
<td>
<input type="text" id="linker_QQ" th:field="*{QQ}" oninput="check('linker_QQ')"
maxlength="10" pattern="[0-9]{8,10}"
placeholder="请输入QQ号" required/>
</td>
<td><p id="pqq">请输入QQ</p></td>
</tr>
</table>
<br>
<input type="submit" id="sub" value="提交修改"
style="margin: 0 auto;">
</form>
</div>
</body>
</html>
最终效果
成功/错误的登录
添加联系人
编辑联系人
附:关于外部引用的js文件
CSS的样式大家可以根据自己喜好来,js的逻辑见下:
add.js
这个时add.html和change.html通用的,因为逻辑时一模一样的
function check(id)
{
var elem = document.getElementById(id); //获取需要检查的标签元素
var content = elem.value; //获取该标签内容
var temp = null;
var pattern = elem.pattern; //获取该标签已经设置好的正则匹配规则
var regex = new RegExp('^' + pattern + '$'); //将pattern加上头尾标识
var match = regex.exec(content); //进行正则匹配
if (id == 'linker_Name')
temp = document.getElementById('pname');
else if (id == 'linker_Phone')
temp = document.getElementById('pphone');
else if (id == 'linker_Email')
temp = document.getElementById('pemail');
else if (id == 'linker_Address')
temp = document.getElementById('paddress');
else if (id == 'linker_QQ')
temp = document.getElementById('pqq');
//内容变为空
if ("" == content && temp!=null)
{
temp.innerHTML = "?";
temp.style.color = "#FFA500";
}
//匹配成功
else if (null != match && temp!=null)
{
temp.innerHTML = "√";
temp.style.color = "#00FF00";
}
//匹配失败
else if (null == match && temp!=null)
{
temp.innerHTML = "×";
temp.style.color = "#FF0000";
}
}
list.js
点击删除、编辑时如何获取当前迭代对象的行呢?在这里可以利用JavaScript
function add() {
var temp = document.createElement("form")
temp.action = "/add"
temp.method = "post"
temp.style.display = "none"
document.body.appendChild(temp)
temp.submit()
}
function exit(){
var temp = document.createElement("form")
temp.action = "/exit"
temp.method = "post"
temp.style.display = "none"
document.body.appendChild(temp)
temp.submit()
}
function change(elem) {
var row = elem.parentNode.parentNode.rowIndex - 1
var temp = document.createElement("form")
temp.action = "/change"
temp.method = "post"
temp.style.display = "none"
//由于form类型的元素没办法设置value值,所以随便创建一个其他类型的元素记录,将row值加载body后面
var current = document.createElement("textarea")
current.name = "row"
current.value = row.toString()
temp.appendChild(current)
document.body.appendChild(temp)
temp.submit()
}
function del(elem) {
//第一次parentNode得到td标签的位置,第二次parentNode得到tr标签的位置
var row = elem.parentNode.parentNode.rowIndex -1
var tr = elem.parentNode.parentNode
var tbody = tr.parentNode
var temp = document.createElement("form")
temp.action = "/del"
temp.method = "post"
temp.style.display = "none"
var opt = document.createElement("textarea")
opt.name = "row"
opt.value = row.toString()
temp.appendChild(opt)
document.body.appendChild(temp)
//找到tr标签后删掉
tbody.removeChild(tr)
temp.submit()
}