完善登录功能
新增员工
员工信息分页查询
启用/禁用员工账号
编辑员工信息
分析前端页面效果是如何实现的
为什么点击左边 右边会根着变化
首先 我们先来看一下菜单是如何展示出来的 在来看一下 为啥点击菜单时 右边会跟着变
第一 :菜单是如何展示出来的
我们在创建一个VUE对象的时候 在我们的 data() 的位置 我们准备了 menuList: [] 的一个数据 menuList: [] 是一个数组对象 数组对象里面放着一个个JSON数据 而每一个JSON数据里面又放着id name url icon(图标) 以上只是数据的准备 并不是定义就可以展示到菜单的一个位置
如果要展示到菜单的位置
请到第39行 用到了el-menu组件 下面的v-for表示在遍历 遍历menuList
重启
刷新
为什么是Id 因为由这个对象数组menuList:[]决定
`new Vue({
el: '#app',
data() {
return {
defAct: '2',
menuActived: '2',
userInfo: {},
menuList: [
// {
// id: '1',
// name: '门店管理',
// children: [
{
id: '2',
name: '员工管理',
url: 'page/member/list.html',
icon: 'icon-member'
},
{
id: '3',
name: '分类管理',
url: 'page/category/list.html',
icon: 'icon-category'
},
{
id: '4',
name: '菜品管理',
url: 'page/food/list.html',
icon: 'icon-food'
},
{
id: '5',
name: '套餐管理',
url: 'page/combo/list.html',
icon: 'icon-combo'
},
{
id: '6',
name: '订单明细',
url: 'page/order/list.html',
icon: 'icon-order'
}
// ],
// },
],
{{item.name}}
{{sub.name}}
{{item.name}}
第二 : 点击菜单时 右边会跟着变
http://localhost:8080/backend/index.html
这些菜单实际上都加了一个@click事件 当我们点击菜单的事情 他会执行menuHandle() 方法
然后把item,false传过来 接下来我们来把menuHandle() 方法 在202行
menuHandle(item, goBackFlag) {
this.loading = true
this.menuActived = item.id
this.iframeUrl = item.url
this.headTitle = item.name
this.goBackFlag = goBackFlag
this.closeLoading()
},
当我们点击菜单的时候 就会执行menuHandle() 方法 这里面最重要的是 this.iframeUrl = item.url 这里面的url就是以下url 也就是点击菜单的url
menuList: [
// {
// id: '1',
// name: '门店管理',
// children: [
{
id:'2',
name : '员工管理',
url: 'page/member/list.html',
icon: 'icon-member'
},
{
id: '3',
name: '分类管理',
url: 'page/category/list.html',
icon: 'icon-category'
},
{
id: '4',
name: '菜品管理',
url: 'page/food/list.html',
icon: 'icon-food'
},
{
id: '5',
name: '套餐管理',
url: 'page/combo/list.html',
icon: 'icon-combo'
},
{
id: '6',
name: '订单明细',
url: 'page/order/list.html',
icon: 'icon-order'
}
// ],
// },
],
这里面是通过iframeUrl的方式来显示新的页面
iframeUrl在第164行定义的
iframeUrl: 'page/member/list.html',
在第95行用到 这个是用来展示一个新的页面的 而这个页面从哪里来呢
:src="iframeUrl"
iframeUrl传进来什么数据 就展示什么数据 为什么我们登录之后 展示的是员工管理 而不是分类管理或是其他
这是因为我们给iframeUrl一个初始值
iframeUrl: 'page/member/list.html',
而我们在点击菜单的时候 其实是在切换url
menuHandle(item, goBackFlag) {
this.loading = true
this.menuActived = item.id
this.iframeUrl = item.url
this.headTitle = item.name
this.goBackFlag = goBackFlag
this.closeLoading()
},
比如说我们现在初始页面给修改成以下
iframeUrl: 'http://www.itcast.c
重启 刷新
所以当我们点击菜单的时候 其实他是在切换url地址
menuHandle(item, goBackFlag) {
this.loading = true
this.menuActived = item.id
this.iframeUrl = item.url
this.headTitle = item.name
this.goBackFlag = goBackFlag
this.closeLoading()
},
而这个url地址在数据准备的时候就已经写好了
menuList: [
// {
// id: '1',
// name: '门店管理',
// children: [
{
id:'2',
name : '员工管理',
url: 'page/member/list.html',
icon: 'icon-member'
},
{
id: '3',
name: '分类管理',
url: 'page/category/list.html',
icon: 'icon-category'
},
{
id: '4',
name: '菜品管理',
url: 'page/food/list.html',
icon: 'icon-food'
},
{
id: '5',
name: '套餐管理',
url: 'page/combo/list.html',
icon: 'icon-combo'
},
{
id: '6',
name: '订单明细',
url: 'page/order/list.html',
icon: 'icon-order'
}
// ],
// },
],
第64行
第202行-第209行
menuHandle(item, goBackFlag) {
this.loading = true
this.menuActived = item.id
this.iframeUrl = item.url
this.headTitle = item.name
this.goBackFlag = goBackFlag
this.closeLoading()
},
第164行
iframeUrl: 'page/member/list.html',
第95行-104行
1. 完善登录功能
1.1 问题分析
前面我们已经完成了后台系统的员工登录功能开发,但是目前还存在一个问题,接下来我们来说明一个这个问题, 以及如何处理。
1). 目前现状
用户如果不登录,直接访问系统首页面,照样可以正常访问。
2). 理想效果
上述这种设计并不合理,我们希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,如果没有登录, 访问系统中的任何界面都直接跳转到登录页面。
那么,具体应该怎么实现呢?
可以使用我们之前讲解过的 过滤器、拦截器来实现,在过滤器、拦截器中拦截前端发起的请求,判断用户是否已经完成登录,如果没有登录则返回提示信息,跳转到登录页面。
1.2 思路分析
过滤器具体的处理逻辑如下:
A. 获取本次请求的URI
B. 判断本次请求, 是否需要登录, 才可以访问
C. 如果不需要,则直接放行
D. 判断登录状态,如果已登录,则直接放行
E. 如果未登录, 则返回未登录结果
如果未登录,我们需要给前端返回什么样的结果呢? 这个时候, 我们可以去看看前端是如何处理的 ?
1.3 代码实现
1). 定义登录校验过滤器
自定义一个过滤器 LoginCheckFilter 并实现 Filter 接口, 在doFilter方法中完成校验的逻辑。 那么接下来, 我们就根据上述分析的步骤, 来完成具体的功能代码实现:
所属包: com.itheima.reggie.filter
/**
* 检查用户是否已经完成登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//强转
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info("拦截到请求:{}",request.getRequestURI());
//放行
重启 刷新
/**
* 检查用户是否已经完成登录
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1、获取本次请求的URI
String requestURI = request.getRequestURI();// /backend/index.html
log.info("拦截到请求:{}",requestURI);
//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3、如果不需要处理,则直接放行
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
//4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
//5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if(match){
return true;
}
}
return false;
}}
复习
第一:getAttribute的理解
getAttribute()是在Java Web应用中获取请求中所包含的属性的方法,可以通过HttpServletRequest对象调用。
在Java Web应用中,一个请求中可能会包含一些附加的属性信息,这些属性信息可以通过setAttribute()方法设置,然后在请求处理的过程中使用getAttribute()方法获取。比如,在一个登录页面中,可以将用户输入的用户名和密码设置为请求属性,在后续的处理过程中获取这些属性信息进行用户验证。
在具体的使用过程中,getAttribute()方法需要传入一个String类型的参数,该参数表示要获取的属性的名称。如果指定的属性存在,则该方法返回该属性的值,否则返回null。需要注意的是,返回值是Object类型,需要进行相应的强制类型转换。
需要注意的是,getAttribute()方法只能获取请求中所包含的属性信息,无法获取Session属性或ServletContext属性。如果要获取Session或ServletContext属性,需要使用getSessionAttribute()或getServletContextAttribute()方法。
getAttribute的理解 举例说明
假设在一个Java Web应用中,有一个登录页面,用户在该页面输入用户名和密码后提交表单,服务器端接收到请求后进行用户验证,如果验证通过,则跳转到用户首页。
在这个过程中,可以将用户输入的用户名和密码设置为请求属性,然后在后续的处理过程中使用getAttribute()方法获取这些属性信息,以完成用户验证。具体的代码可以如下:
`// 登录页面处理逻辑
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取用户输入的用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
// 将用户名和密码设置为请求属性
request.setAttribute("username", username);
request.setAttribute("password", password);
// 进行用户验证,如果验证通过则跳转到用户首页
if (validateUser(username, password)) {
// 获取请求属性中的用户名信息
String usernameAttr = (String) request.getAttribute("username");
// 获取请求属性中的密码信息
String passwordAttr = (String) request.getAttribute("password");
// 输出请求属性中的信息
System.out.println("Username: " + usernameAttr);
System.out.println("Password: " + passwordAttr);
// 跳转到用户首页
response.sendRedirect("/userhome.jsp");
} else {
// 用户验证不通过,返回登录页面
response.sendRedirect("/login.jsp");
}
}
在上述代码中,首先获取用户输入的用户名和密码,并将它们设置为请求属性。接着,使用getAttribute()方法获取请求属性中的用户名和密码信息,并输出到控制台。最后,如果用户验证通过,则使用sendRedirect()方法跳转到用户首页。如果用户验证不通过,则返回登录页面。
需要注意的是,getAttribute()方法获取的是Object类型的属性值,需要进行强制类型转换。另外,如果请求属性不存在,getAttribute()方法会返回null。
第二: response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));理解
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")))是在Java Web应用中使用JSON格式返回错误信息的一种常用方式。具体来说,它通过将一个错误信息转换成JSON格式,然后通过response对象返回给客户端,实现了错误信息的传递。
具体而言,response.getWriter()是通过HttpServletRequest对象获取到一个PrintWriter对象,可以将字符数据发送到客户端。write()方法是PrintWriter对象中的方法,用于输出字符数据。
在上述代码中,JSON.toJSONString()方法用于将一个R对象转换成JSON格式的字符串。R是一个封装了返回结果的类,其中包含了错误码、错误信息、返回数据等信息。R.error()是一个静态方法,用于返回一个错误的R对象,其中包含了一个错误码和错误信息。在这里,错误码为NOTLOGIN,表示用户未登录。
因此,response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")))的含义是:将一个包含错误码和错误信息的R对象转换成JSON格式的字符串,然后通过response对象返回给客户端。客户端可以根据返回的JSON格式的字符串,获取到错误信息,并进行相应的处理。
AntPathMatcher 拓展:
介绍: Spring中提供的路径匹配器 ;
通配符规则:
符号
含义
?
匹配一个字符
*
匹配0个或多个字符
**
匹配0个或多个目录/字符
2). 开启组件扫描
需要在引导类上, 加上Servlet组件扫描的注解, 来扫描过滤器配置的@WebFilter注解, 扫描上之后, 过滤器在运行时就生效了。
@ServletComponentScan 的作用:
在SpringBoot项目中, 在引导类/配置类上加了该注解后, 会自动扫描项目中(当前包及其子包下)的@WebServlet , @WebFilter , @WebListener 注解, 自动注册Servlet的相关组件 ;
@Slf4j
@SpringBootApplication
@ServletComponentScan
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class, args);
log.info("项目启动成功...");
}
}
1.4 功能测试
代码编写完毕之后,我们需要将工程重启一下,然后在浏览器地址栏直接输入系统管理后台首页
(http://localhost:8080/backend/index.html),然后看看是否可以跳转到登录页面即可。我们也可以通过debug的形式来跟踪一下代码执行的过程。
对于前端的代码, 也可以进行debug调试。
F12打开浏览器的调试工具, 找到我们前面提到的request.js, 在request.js的响应拦截器位置打上断点。
访问http://localhost:8080/backend/index.html
取消断点
2. 新增员工
2.1 需求分析
后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击[添加员工]按钮跳转到新增页面,如下:
当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。
2.2 数据模型
新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。employee表中的status字段已经设置了默认值1,表示状态正常。
需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的。
2.3 程序执行流程
在开发代码之前,我们需要结合着前端页面发起的请求, 梳理一下整个程序的执行过程:
A. 点击"保存"按钮, 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端, 请求方式POST, 请求路径 /employee
B. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
C. Service调用Mapper操作数据库,保存数据
然后我们结合如下页面来看一下请求是怎么发出来的
找到第41行添加员工 然后在39行给这个按钮添加一个单击事件@click="addMemberHandle('add')"
当我点击@click的时候会执行addMemberHandle方法 然后 传进来一个参数叫做add
然后现在我们来找addMemberHandle()方法 在第158行
这样看到的效果就是页面 发生了切换
然后接下来我们来讲解add.html页面
找到76行 当我们点击保存按钮的时候 我们的页面就会提交 当点击保存的时候 会执行submitForm('ruleForm', false)方法
` @click="submitForm('ruleForm', false)"
然后我们找到submitForm()方法
然后找到表单对象 然后进行validate校检 假设我们什么都不填写的话 点击保存 他会给我们提示
如果校检通过 参数就会变成ture 然后我们在来判断一个这个类型是一个add 然后我们就会组装一上JSON对象
` const params = {
...this.ruleForm,
sex: this.ruleForm.sex === '女' ? '0' : '1'
}
然后我们就调用addEmployee()方法 这个方法封装到js文件里面 我们Ctrl+左键
在.js里面把params参数传进来 到add.html
params就是我们说的JSON对象
`const params = {
...this.ruleForm,
sex: this.ruleForm.sex === '女' ? '0' : '1'
}
这样的话 请求就发出来了 请求发出来之后 就会执行我们的服务端 比如 我们的Controller然后调用我们的Service然后调用我们的Mapper 最终调用我们的数据库 最后由我们的Controller给页面一个返回 返回的时候就会执行res 然后在判断我们的code 是否 1 如果code === 1 就会弹出this.$message.success('员工添加成功!')
` submitForm (formName, st) {
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.actionType === 'add') {
const params = {
...this.ruleForm,
sex: this.ruleForm.sex === '女' ? '0' : '1'
}
addEmployee(params).then(res => {
if (res.code === 1) {
this.$message.success('员工添加成功!')
if (!st) {
this.goBack()
} else {
this.ruleForm = {
username: '',
'name': '',
'phone': '',
// 'password': '',
// 'rePassword': '',/
'sex': '男',
'idNumber': ''
}
}
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
} else {
const params = {
...this.ruleForm,
sex: this.ruleForm.sex === '女' ? '0' : '1'
}
editEmployee(params).then(res => {
if (res.code === 1) {
this.$message.success('员工信息修改成功!')
this.goBack()
} else {
this.$message.error(res.msg || '操作失败')
}
}).catch(err => {
this.$message.error('请求出错了:' + err)
})
}
} else {
console.log('error submit!!')
return false
}
})
},
2.4 代码实现
在EmployeeController中增加save方法, 用于保存用户员工信息。
A. 在新增员工时, 按钮页面原型中的需求描述, 需要给员工设置初始默认密码 123456, 并对密码进行MD5加密。
B. 在组装员工信息时, 还需要封装创建时间、修改时间,创建人、修改人信息(从session中获取当前登录用户)。
保证前端页面发送的请求 能够请求到save() 并且我们提交的参数能够封装到employee 所以我们先打个 断点
` /**
* 新增员工
* @param employee
* @return
*/
@PostMapping
public R save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
return null;
}
然后登录http://localhost:8080/backend/page/login/login.html
以上如果说明 数据能够正常封装到employee对象里面 接下来我们来设置密码
/**
* 新增员工
* @param employee
* @return
*/
@PostMapping
public R save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
//设置初始密码123456,需要进行md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
//创建当前时间 类型是LocalDateTime
employee.setCreateTime(LocalDateTime.now());
//创建更新时间 类型是LocalDateTime
employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id 我们登录成功之后 就把用户的id放到JSON里面 所以我们可以用一个request得到一个session
Long empId = (Long) request.getSession().getAttribute("employee");
//创建用户
employee.setCreateUser(empId);
//更新人
employee.setUpdateUser(empId);
//保存
employeeService.save(employee);
//返回
return R.success("新增员工成功");
}
}
理解:Long empId = (Long) request.getSession().getAttribute("employee");
Long empId = (Long) request.getSession().getAttribute("employee"); 请解释一下这段代码
这段代码是在一个Java Web应用程序中获取保存在当前用户会话中名为"employee"的属性值,将其转换为长整型数据类型,并将结果存储在一个名为"empId"的变量中。
具体地说,代码的实现逻辑如下:
通过调用HttpServletRequest对象的getSession()方法获取当前用户的会话对象。
通过调用会话对象的getAttribute()方法,以"employee"为参数,获取名为"employee"的属性值。
由于getAttribute()方法返回的是一个Object对象,因此需要将其转换为长整型数据类型。在这里,使用了Java中的强制类型转换,将Object对象强制转换为Long类型。
最后,将转换后的长整型数据类型的值存储在名为"empId"的变量中,以便在代码的其他部分中使用。
复习一 :session
session是服务器端的技术。服务器为每一个浏览器开辟一块内存空间,即session对象。由于session对象是每一个浏览器特有的,所以用户的记录可以存放在session对象中
数据存储在服务器端
服务器会为每一个客户端创建一个Session对象,该客户端每次访问如果需要session对象,就返回之前创建的
服务器端的会话从第一次获得HttpSession对象开始的,直到HttpSession对象销毁结束
2.5 功能测试
代码编写完毕之后,我们需要将工程重启, 完毕之后直接访问管理系统首页, 点击 "员工管理" 页面中的 "添加员工" 按钮, 输入员工基本信息, 然后点击 "保存" 进行数据保存, 保存完毕后, 检查数据库中是否录入员工数据。
`http://localhost:8080/backend/page/login/login.html
原来
保存添加一个员工后
当我们在测试中,添加用户时, 输入了一个已存在的用户名时,前端界面出现错误提示信息:
而此时,服务端已经报错了, 报错信息如下:
输入一个已存在的用户名
前端界面出现错误提示信息
服务端已经报错了, 报错信息如下:
### Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'zhangsan' for key 'idx_username'
; Duplicate entry 'zhangsan' for key 'idx_username'; nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'zhangsan' for key 'idx_username'] with root cause
出现上述的错误, 主要就是因为在 employee 表结构中,我们针对于username字段,建立了唯一索引,添加重复的username数据时,违背该约束,就会报错。但是此时前端提示的信息并不具体,用户并不知道是因为什么原因造成的该异常,我们需要给用户提示详细的错误信息 。
2.6 全局异常处理
2.6.1 思路分析
要想解决上述测试中存在的问题,我们需要对程序中可能出现的异常进行捕获,通常有两种处理方式:
A. 在Controller方法中加入 try...catch 进行异常捕获
形式如下:
如果采用这种方式,虽然可以解决,但是存在弊端,需要我们在保存其他业务数据时,也需要在Controller方法中加上try...catch进行处理,代码冗余,不通用。
B. 使用异常处理器进行全局异常捕获
采用这种方式来实现,我们只需要在项目中定义一个通用的全局异常处理器,就可以解决本项目的所有异常。
复习一:@ResponseBody解释一下这个注解
@ResponseBody是Spring框架中的一个注解,用于将方法的返回值转换成指定的格式,并作为HTTP响应的主体返回给客户端。
在Spring中,控制器方法可以返回不同类型的值,例如视图名称、模型数据、JSON对象等等。当控制器方法使用@ResponseBody注解时,Spring框架将不会将返回值解析为视图名称或模型数据,而是将其转换成指定格式的数据并直接返回给客户端。
@ResponseBody注解的作用是告诉Spring框架将方法的返回值转换成指定格式的数据(如JSON、XML等),并将其放入HTTP响应中,而不是解析为视图名称或模型数据。通常用于返回RESTful API的响应结果。
例如,下面的代码演示了一个使用@ResponseBody注解的控制器方法,该方法返回一个JSON格式的对象:
`@RestController
public class UserController {
@GetMapping("/user/{id}")
@ResponseBody
public User getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return user;
}
}
复习二 :
理解@ControllerAdvice注解
@ControllerAdvice 是一个 Spring MVC 注解,用于全局控制器的异常处理和数据绑定。在一个应用程序中,有可能会有多个控制器(Controller),如果每个控制器都需要相同的异常处理和数据绑定,那么就需要在每个控制器中都进行相同的配置,这样会造成代码冗余。@ControllerAdvice 就是为了解决这个问题而产生的,它可以实现全局的异常处理和数据绑定,将控制器中的相同代码抽离到一个全局的类中,从而减少代码冗余。
@ControllerAdvice 注解可以用来定义一个类,这个类可以包含多个注解方法。这些注解方法可以处理控制器中抛出的异常,也可以在控制器中返回数据时进行数据绑定。在处理异常时,可以使用 @ExceptionHandler 注解来标记处理异常的方法,这些方法可以处理特定类型的异常。在进行数据绑定时,可以使用 @ModelAttribute 和 @InitBinder 注解来标记数据绑定的方法,这些方法可以处理控制器中的请求参数和模型属性。
使用 @ControllerAdvice 注解可以大大简化控制器中的代码,让控制器只关注业务逻辑,将异常处理和数据绑定交给全局的类处理。这样可以提高代码的可读性和可维护性。
理解:@ControllerAdvice(annotations = {RestController.class, Controller.class})
这行代码是一个Spring注解,用于定义一个全局的异常处理器。其中,@ControllerAdvice注解表示该类是一个全局异常处理器,并且它会拦截带有@RestController和@Controller注解的控制器中抛出的异常。也就是说,当控制器中发生异常时,这个注解所标注的类中的方法会被自动调用,以处理这些异常,返回给客户端相应的错误信息。
复习三 理解 1 @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
`@ExceptionHandler 是 Spring Framework 提供的一种异常处理机制,可以用于处理应用程序中抛出的特定类型的异常。SQLIntegrityConstraintViolationException 是一个特定的异常类,用于表示在 SQL 操作期间违反了完整性约束的情况,例如试图插入重复的数据或者试图在有外键关联的表中删除或更新数据。
因此,@ExceptionHandler(SQLIntegrityConstraintViolationException.class) 意味着这个方法会被调用来处理抛出 SQLIntegrityConstraintViolationException 异常的情况。在 Spring MVC 应用程序中,如果控制器方法抛出此类异常,Spring 将使用带有 @ExceptionHandler 注释的方法来处理它,并返回相应的错误响应。通常情况下,此方法会返回一个适当的 HTTP 状态码和错误消息,以便客户端能够了解到发生了什么,并采取适当的措施。
`@ExceptionHandler(SQLIntegrityConstraintViolationException.class)是一个Spring注解,用于在处理控制器方法时捕获SQLIntegrityConstraintViolationException类型的异常。
当控制器方法中抛出指定类型的异常时,Spring会自动调用被@ExceptionHandler注解标记的方法来处理该异常,从而提供给用户更友好的错误提示。在这个例子中,SQLIntegrityConstraintViolationException通常由数据库中的完整性约束(例如唯一性约束、外键约束等)引起,指示试图插入或更新的数据违反了某些约束条件。
通过使用@ExceptionHandler注解,我们可以针对不同的异常类型编写专门的处理逻辑,并将其集中在一个地方,以便于管理和维护。这种方式可以使应用程序的代码更加健壮和可读,并提高应用程序的容错能力。
理解2:@ExceptionHandler
@ExceptionHandler 是 Spring Framework 中的一个注解,它用于定义处理特定异常类型的方法。当在处理请求期间发生指定异常时,Spring 将调用带有 @ExceptionHandler 注解的方法来处理该异常。
在处理请求期间,如果发生异常并且没有被捕获,则将抛出该异常。但是,如果您在控制器中使用了 @ExceptionHandler 注解,则可以捕获特定异常并使用自定义逻辑进行处理。这可以用来优雅地处理应用程序中的异常,而不会让用户看到不友好的错误信息或堆栈跟踪。
例如,以下代码演示了如何使用 @ExceptionHandler 注解来处理特定类型的异常:
`@Controller
public class MyController {
@ExceptionHandler(SomeException.class)
public ResponseEntity handleSomeException(SomeException ex) {
// 处理 SomeException 异常的逻辑
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Something went wrong.");
}
}
在这个例子中,@ExceptionHandler 注解标记的 handleSomeException 方法将处理 SomeException 异常。如果在处理请求期间发生 SomeException 异常,则 Spring 将调用该方法来处理它,并返回一个自定义的错误响应实体。
2.6.2 全局异常处理器
在项目中自定义一个全局异常处理器,在异常处理器上加上注解 @ControllerAdvice,可以通过属性annotations指定拦截哪一类的Controller方法。 并在异常处理器的方法上加上注解 @ExceptionHandler 来指定拦截的是那一类型的异常。
异常处理方法逻辑:
指定捕获的异常类型为 SQLIntegrityConstraintViolationException
解析异常的提示信息, 获取出是那个值违背了唯一约束
组装错误信息并返回
所属包: com.itheima.reggie.common
/**
* 全局异常处理
*/
//@ControllerAdvice注解表示该类是一个全局异常处理器,
// 并且它会拦截带有@RestController和@Controller注解的控制器中抛出的异常。
// 也就是说,当控制器中发生异常时,这个注解所标注的类中的方法会被自动调用,以处理这些异常,返回给客户端相应的错误信息。
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
//一旦Controller 发生异常这个SQLIntegrityConstraintViolationException 就会被拦截到 然后统一在exceptionHandler()方法中进行处理
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
return R.error("失败了");
}
}
测试
http://localhost:8080/backend/page/login/login.html
F8
F9 放行
`/**
* 全局异常处理
*/
//@ControllerAdvice注解表示该类是一个全局异常处理器,
// 并且它会拦截带有@RestController和@Controller注解的控制器中抛出的异常。
// 也就是说,当控制器中发生异常时,这个注解所标注的类中的方法会被自动调用,以处理这些异常,返回给客户端相应的错误信息。
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
//一旦Controller 发生异常这个SQLIntegrityConstraintViolationException 就会被拦截到 然后统一在exceptionHandler()方法中进行处理
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
测试:
注解说明:
上述的全局异常处理器上使用了的两个注解 @ControllerAdvice , @ResponseBody , 他们的作用分别为:
@ControllerAdvice : 指定拦截那些类型的控制器;
@ResponseBody: 将方法的返回值 R 对象转换为json格式的数据, 响应给页面;
上述使用的两个注解, 也可以合并成为一个注解 @RestControllerAdvice
2.6.3 测试
全局异常处理器编写完毕之后,我们需要将项目重启, 完毕之后直接访问管理系统首页, 点击 "员工管理" 页面中的 "添加员工" 按钮。当我们在测试中,添加用户时, 输入了一个已存在的用户名时,前端界面出现如下错误提示信息:
3. 员工分页查询
3.1 需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。而在我们的分页查询页面中, 除了分页条件以外,还有一个查询条件 "员工姓名"。
请求参数
搜索条件: 员工姓名(模糊查询)
分页条件: 每页展示条数 , 页码
响应数据
总记录数
结果列表
3.2 程序执行流程
3.2.1 页面流程分析
在开发代码之前,需要梳理一下整个程序的执行过程。
A. 点击菜单,打开员工管理页面时,执行查询:
B. 搜索栏输入员工姓名,回车,执行查询:
1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
3). Service调用Mapper操作数据库,查询分页数据
4). Controller将查询到的分页数据, 响应给前端页面
5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
3.2.2 前端代码介绍
. 访问员工列表页面/member/list.html时, 会触发Vuejs中的钩子方法, 在页面初始化时调用created方法
从上述的前端代码中我们可以看到, 执行完分页查询, 我们需要给前端返回的信息中需要包含两项 : records 中封装结果列表, total中封装总记录数 。
而在组装请求参数时 , page、pageSize 都是前端分页插件渲染时的参数;
. 在getMemberList方法中, 通过axios发起异步请求
axios发起的异步请求会被声明在 request.js 中的request拦截器拦截, 在其中对get请求进行进一步的封装处理
最终发送给服务端的请求为 : GET请求 , 请求链接 /employee/page?page=1&pageSize=10&name=xxx
3.3 代码实现
3.3.1 分页插件配置
当前我们要实现的分页查询功能,而在MybatisPlus要实现分页功能,就需要用到MybatisPlus中提供的分页插件,要使用分页插件,就要在配置类中声明分页插件的bean对象。
3.3.2 分页查询实现
在上面我们已经分析了,页面在进行分页查询时, 具体的请求信息如下:
请求
说明
请求方式
GET
请求路径
/employee/page
请求参数
page , pageSize , name
那么查询完毕后我们需要给前端返回什么样的结果呢?
在上述我们也分析了, 查询返回的结果数据data中应该封装两项信息, 分别为: records 封装分页列表数据, total 中封装符合条件的总记录数。 那么这个时候, 在定义controller方法的返回值类型R时, 我们可以直接将 MybatisPlus 分页查询的结果 Page 直接封装返回, 因为Page中的属性如下:
那么接下来就依据于这些已知的需求和条件完成分页查询的代码实现。 具体的逻辑如下:
A. 构造分页条件
B. 构建搜索条件 - name进行模糊匹配
C. 构建排序条件 - 更新时间倒序排序
D. 执行查询
E. 组装结果并返回
具体的代码实现如下:
3.4 功能测试
代码编写完毕之后,我们需要将工程重启, 完毕之后直接访问管理系统首页, 默认就会打开员工管理的列表页面, 我们可以查看列表数据是否可以正常展示, 也可以通过分页插件来测试分页功能, 及员工姓名的模糊查询功能。
在进行测试时,可以使用浏览器的监控工具查看页面和服务端的数据交互细节。 并借助于debug的形式, 根据服务端参数接收及逻辑执行情况。
测试过程中可以发现,对于员工状态字段(status)服务端返回的是状态码(1或者0),但是页面上显示的则是“正常”或者“已禁用”,这是因为页面中在展示数据时进行了处理。
4. 启用/禁用员工账号
4.1 需求分析
在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 "禁用",如果员工账号状态为已禁用,则按钮显示为"启用"。
==需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。==
A. admin 管理员登录
B. 普通用户登录
4.2 程序执行流程
4.2.1 页面按钮动态展示
在上述的需求中,我们提到需要实现的效果是 : 只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示 , 页面中是怎么做到只有管理员admin能够看到启用、禁用按钮的?
1). 在列表页面(list.html)加载时, 触发钩子函数created, 在钩子函数中, 会从localStorage中获取到用户登录信息, 然后获取到用户名
2). 在页面中, 通过Vue指令v-if进行判断,如果登录用户为admin将展示 启用/禁用 按钮, 否则不展示
4.2.2 执行流程分析
. 当管理员admin点击 "启用" 或 "禁用" 按钮时, 调用方法statusHandle
scope.row : 获取到的是这一行的数据信息 ;
statusHandle方法中进行二次确认, 然后发起ajax请求, 传递id、status参数
最终发起异步请求, 请求服务端, 请求信息如下:
请求
说明
请求方式
PUT
请求路径
/employee
请求参数
{"id":xxx,"status":xxx}
{...params} : 三点是ES6中出现的扩展运算符。作用是遍历当前使用的对象能够访问到的所有属性,并将属性放入当前对象中。
4.3 代码实现
在开发代码之前,需要梳理一下整个程序的执行过程:
1). 页面发送ajax请求,将参数(id、status)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service更新数据
3). Service调用Mapper操作数据库
启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作。在Controller中创建update方法,此方法是一个通用的修改员工信息的方法。
4.4 功能测试
代码编写完毕之后,我们需要将工程重启。 然后访问前端页面, 进行 "启用" 或 "禁用" 的测试。
测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。但是从控制台输出的日志, 可以看出确实没有更新成功。
而在我们的数据库表结构中, 并不存在该ID, 数据库中 风清扬 对应的ID为 1420038345634918401
4.5 代码修复
4.5.1 原因分析
通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致,这是怎么回事呢?
在分页查询时,服务端会将返回的R对象进行json序列化,转换为json格式的数据,而员工的ID是一个Long类型的数据,而且是一个长度为 19 位的长整型数据, 该数据返回给前端是没有问题的。
那么具体的问题出现在哪儿呢?
问题实际上, 就出现在前端JS中, js在对长度较长的长整型数据进行处理时, 会损失精度, 从而导致提交的id和数据库中的id不一致。 这里,我们也可以做一个简单的测试,代码如下:
`
4.5.2 解决方案
要想解决这个问题,也很简单,我们只需要让js处理的ID数据类型为字符串类型即可, 这样就不会损失精度了。同样, 大家也可以做一个测试:
那么在我们的业务中, 我们只需要让分页查询返回的json格式数据库中, long类型的属性, 不直接转换为数字类型, 转换为字符串类型就可以解决这个问题了 , 最终返回的结果为 :
4.5.3 代码修复
由于在SpringMVC中, 将Controller方法返回值转换为json对象, 是通过jackson来实现的, 涉及到SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter, 所以我们要解决这个问题, 就需要对该消息转换器的功能进行拓展。
具体实现步骤:
1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
2). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
1). 引入JacksonObjectMapper
2). 在WebMvcConfig中重写方法extendMessageConverters
5. 编辑员工信息
5.1 需求分析
在员工管理列表页面点击 "编辑" 按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击 "保存" 按钮完成编辑操作。
那么从上述的分析中,我们可以看出当前实现的编辑功能,我们需要实现两个方法:
A. 根据ID查询, 用于页面数据回显
B. 保存修改
5.2 程序执行流程
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1). 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2). 在add.html页面获取url中的参数[员工id]
3). 发送ajax请求,请求服务端,同时提交员工id参数
4). 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5). 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6). 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7). 服务端接收员工信息,并进行处理,完成后给页面响应
8). 页面接收到服务端响应信息后进行相应处理
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作
5.3 代码实现
5.3.1 根据ID查询
经过上述的分析,我们看到,在根据ID查询员工信息时,请求信息如下:
请求
说明
请求方式
GET
请求路径
/employee/{id}
代码实现:
在EmployeeController中增加方法, 根据ID查询员工信息。
5.3.2 修改员工
经过上述的分析,我们看到,在修改员工信息时,请求信息如下:
请求
说明
请求方式
PUT
请求路径
/employee
请求参数
{.......} json格式数据
代码实现:
在EmployeeController中增加方法, 根据ID更新员工信息。
5.4 功能测试
代码编写完毕之后,我们需要将工程重启。 然后访问前端页面, 按照前面分析的操作流程进行测试,查看数据是否正常修改即可。