SpringBootWeb案例-员工模块
SpringBootWeb案例-员工模块
今日目标
- 修改员工 ✏️
- 配置管理 🍐
- 统一异常管理 🍐
- 员工信息统计 ✏️
知识准备
- 完成了部门修改,班级修改,员工分页查询,员工插入
- 完成了阿里云工具类封装,并且能上传图片到阿里云
- 知道异常的作用
员工管理
1.1 修改员工之回显
修改员工之回显
需求:修改员工信息

在进行修改员工信息的时候,我们首先先要根据员工的ID查询员工的详细信息用于页面回显展示,然后用户修改员工数据之后,点击保存按钮,就可以将修改的数据提交到服务端,保存到数据库。 具体操作为:
- 根据ID查询员工信息
- 保存修改的员工信息
代码操作

在查询回显 时,既需要查询出员工的基本信息 ,又需要查询出该员工的工作经历信息 。
我们可以先通过一条SQL语句,查询出指定员工的基本信息,及其员工的工作经历信息。SQL如下:
select e.*,
ee.id ee_id,
ee.begin ee_begin,
ee.end ee_end,
ee.company ee_company,
ee.job ee_job
from emp e left join emp_expr ee on e.id = ee.emp_id where e.id = 38;
运行截图:
具体的实现思路如下:

1). EmpController
添加 getInfo
用来根据ID查询员工数据,用于页面回显
/**
* 查询回显
*/
@GetMapping("/{id}")
public Result getInfo(@PathVariable Integer id){
log.info("根据id查询员工的详细信息");
Emp emp = empService.getInfo(id);
return Result.success(emp);
}
2). EmpService
接口中增加 getInfo
方法
/**
* 根据ID查询员工的详细信息
*/
Emp getInfo(Integer id);
3). EmpServiceImpl
实现类中实现 getInfo
方法
@Override
public Emp getInfo(Integer id) {
return empMapper.getById(id);
}
4). EmpMapper
接口中增加 getById
方法
/**
* 根据ID查询员工详细信息
*/
Emp getById(Integer id);
5). EmpMapper.xml
配置文件中定义对应的SQL
<!--自定义结果集ResultMap-->
<resultMap id="empResultMap" type="cn.yangeit.pojo.Emp">
<id column="id" property="id" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="name" property="name" />
<result column="gender" property="gender" />
<result column="phone" property="phone" />
<result column="job" property="job" />
<result column="salary" property="salary" />
<result column="image" property="image" />
<result column="entry_date" property="entryDate" />
<result column="dept_id" property="deptId" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<!--封装exprList-->
<collection property="exprList" ofType="cn.yangeit.pojo.EmpExpr">
<id column="ee_id" property="id"/>
<result column="ee_company" property="company"/>
<result column="ee_job" property="job"/>
<result column="ee_begin" property="begin"/>
<result column="ee_end" property="end"/>
<result column="ee_empid" property="empId"/>
</collection>
</resultMap>
<!--根据ID查询员工的详细信息-->
<select id="getById" resultMap="empResultMap">
select e.*,
ee.id ee_id,
ee.emp_id ee_empid,
ee.begin ee_begin,
ee.end ee_end,
ee.company ee_company,
ee.job ee_job
from emp e left join emp_expr ee on e.id = ee.emp_id
where e.id = #{id}
</select>
在这种一对多的查询中,我们要想成功的封装的结果,需要手动 的基于 <resultMap>
来进行封装结果。
- Mybatis中封装查询结果,什么时候用 resultType,什么时候用resultMap ?
- 如果查询返回的字段名与实体的属性名可以直接对应上,用resultType 。
- 如果查询返回的字段名与实体的属性名对应不上 ,或实体属性比较复杂,可以通过resultMap手动封装 。
Apifox测试
重新启动服务,基于Apifox进行接口测试。

前后端联调测试
打开浏览器,进行前后端联调测试。

总结
课堂作业
- 根据上述提示,完成回显代码!🎤
- resultMap 和取别名,驼峰命名的区别?
2.2 修改员工
前言
查询回显之后,就可以在页面上修改员工的信息了。

当用户修改完数据之后,点击保存按钮,就需要将数据提交到服务端,然后服务端需要将修改后的数据更新到数据库中 。
而此次更新的时候,既需要更新员工的基本信息; 又需要更新员工的工作经历信息 。
代码操作
1). EmpController
增加 update
方法接收请求参数,响应数据
/**
* 更新员工信息
*/
@PutMapping
public Result update(@RequestBody Emp emp){
log.info("修改员工信息, {}", emp);
empService.update(emp);
return Result.success();
}
2). EmpService
接口增加 update
方法
/**
* 更新员工信息
* @param emp
*/
void update(Emp emp);
3). EmpServiceImpl
实现类实现 update
方法
@Transactional
@Override
public void update(Emp emp) {
//1. 根据ID更新员工基本信息
emp.setUpdateTime(LocalDateTime.now());
empMapper.updateById(emp);
//2. 根据员工ID删除员工的工作经历信息 【删除老的】
empExprMapper.deleteByEmpIds(Arrays.asList(emp.getId()));
//3. 新增员工的工作经历数据 【新增新的】
Integer empId = emp.getId();
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){
exprList.forEach(empExpr -> empExpr.setEmpId(empId));
empExprMapper.insertBatch(exprList);
}
}
4). EmpMapper
接口中增加 updateById
方法
/**
* 更新员工基本信息
*/
void updateById(Emp emp);
5). EmpMapper.xml
配置文件中定义对应的SQL语句,基于动态SQL更新员工信息
<!--根据ID更新员工信息-->
<update id="updateById">
update emp
<set>
<if test="username != null and username != ''">username = #{username},</if>
<if test="password != null and password != ''">password = #{password},</if>
<if test="name != null and name != ''">name = #{name},</if>
<if test="gender != null">gender = #{gender},</if>
<if test="phone != null and phone != ''">phone = #{phone},</if>
<if test="job != null">job = #{job},</if>
<if test="salary != null">salary = #{salary},</if>
<if test="image != null and image != ''">image = #{image},</if>
<if test="entryDate != null">entry_date = #{entryDate},</if>
<if test="deptId != null">dept_id = #{deptId},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</set>
where id = #{id}
</update>
2.2.4 Apifox测试
重新启动服务,打开 Apifox 进行接口测试。

2.2.5 前后端联调测试

点击保存之后,查看更新后的数据。

2. 配置文件
员工管理的新增和修改功能我们已开发完成,但在我们所开发的程序中还一些小问题,下面我们就来分析一下当前案例中存在的问题以及如何优化解决。
配置文件
参数配置化

在我们之前编写的程序中进行文件上传时,需要指定两个参数:
- endpoint //阿里云OSS域名
- bucket //存储空间的名字
关于以上的这些阿里云相关配置信息,我们是直接写死在java代码中了(硬编码),如果我们在做项目时每涉及到一个第三方技术服务,就将其参数硬编码,那么在Java程序中会存在两个问题:
- 如果这些参数发生变化了,就必须在源程序代码中改动这些参数,然后需要重新进行代码的编译,将Java代码编译成class字节码文件再重新运行程序。(比较繁琐)
- 如果我们开发的是一个真实的企业级项目, Java类可能会有很多,如果将这些参数分散的定义在各个Java类当中,我们要修改一个参数值,我们就需要在众多的Java代码当中来定位到对应的位置,再来修改参数,修改完毕之后再重新编译再运行。(参数配置过于分散,是不方便集中的管理和维护)
为了解决以上分析的问题,我们可以将参数配置在配置文件中。如下:
#顶格写
#阿里云配置统一写在配置文件中,方便管理
aliyun:
oss:
endpoint: https://oss-cn-wuhan-lr.aliyuncs.com
access-key-id: LTAI5tGJKWuBifboPQrMQFze
access-key-secret: PghR2lcWRjJI4UupdqLtPVXCIz9INS
bucket-name: java86
在将阿里云OSS配置参数交给properties配置文件来管理之后,我们的 UploadController 就变为以下形式:

因为 application.yml
是springboot项目配置文件,所以springboot程序在启动时会默认读取 application.yml
配置文件,而我们可以使用一个现成的注解:@Value
,获取配置文件中的数据。
@Value
注解通常用于外部配置的属性注入,具体用法为: @Value("${配置文件中的key}")
@Component // 表明当前类需要被spring进行实例化,放到ioc容器中
public class AliOSSUtils {
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.access-key-id}")
private String accessKeyId;
@Value("${aliyun.oss.access-key-secret}")
private String accessKeySecret;
@Value("${aliyun.oss.bucket-name}")
private String bucketName;
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile multipartFile) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = multipartFile.getInputStream();
省略代码。。。
}

在springboot项目当中是支持多种配置方式的,除了支持yml配置文件以外,还支持另外一种类型的properties配置文件,就是我们接下来要讲解的yml和properties格式的配置文件。
application.properties
server.port=8080 server.address=127.0.0.1
application.yml
server: port: 8080 address: 127.0.0.1
application.yaml
server: port: 8080 address: 127.0.0.1
yml 格式的配置文件,后缀名有两种:
- yml (推荐)
- yaml
常见配置文件格式对比:

我们可以看到配置同样的数据信息,yml格式的数据有以下特点:
- 容易阅读
- 容易与脚本语言交互
- 以数据为核心,重数据轻格式
简单的了解过springboot所支持的配置文件,以及不同类型配置文件之间的优缺点之后,接下来我们就来了解下yml配置文件的基本语法:
- 大小写敏感
- 数值前边必须有空格,作为分隔符
- 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)
- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
#
表示注释,从这个字符一直到行尾,都会被解析器忽略
yml文件中常见的数据格式。在这里我们主要介绍最为常见的两类:
- 定义对象或Map集合
- 定义数组、list或set集合
对象/Map集合
user:
name: zhangsan
age: 18
password: 123456
数组/List/Set集合
hobby:
- java
- game
- sport
@ConfigurationProperties
讲解完了yml配置文件之后,最后再来介绍一个注解 @ConfigurationProperties
。在介绍注解之前,我们先来看一个场景,分析下代码当中可能存在的问题:

我们在 application.properties
或者 application.yml
中配置了阿里云OSS的两项参数之后,如果java程序中需要这四项参数数据,我们直接通过 @Value
注解来进行注入。这种方式本身没有什么问题,但是如果说需要注入的属性较多(例:需要20多个参数数据),我们写起来就会比较繁琐。
那么有没有一种方式可以简化这些配置参数的注入呢?答案是肯定有,在Spring中给我们提供了一种简化方式,可以直接将配置文件中配置项的值自动的注入到对象的属性中。
Spring提供的简化方式套路:
- 需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致
比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法
- 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象
- 在实体类上添加
@ConfigurationProperties
注解,并通过perfect属性来指定配置参数项的前缀

实体类:AliyunOSSProperties
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
private String endpoint;
private String bucketName;
private String accessKeyId;
private String accessKeySecret;
}
AliOSSUtils工具类:
@Component // 表明当前类需要被spring进行实例化,放到ioc容器中
public class AliOSSUtils {
// @Value("${aliyun.oss.endpoint}")
// private String endpoint;
// @Value("${aliyun.oss.access-key-id}")
// private String accessKeyId;
// @Value("${aliyun.oss.access-key-secret}")
// private String accessKeySecret;
// @Value("${aliyun.oss.bucket-name}")
// private String bucketName;
@Autowired
private AliyunOSSProperties aliyunOSSProperties;
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile multipartFile) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = multipartFile.getInputStream();
// 避免文件覆盖
String originalFilename = multipartFile.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(aliyunOSSProperties.getEndpoint(), aliyunOSSProperties.getAccessKeyId(), aliyunOSSProperties.getAccessKeySecret());
ossClient.putObject(aliyunOSSProperties.getBucketName(), fileName, inputStream);
//文件访问路径
String url = aliyunOSSProperties.getEndpoint().split("//")[0] + "//" + aliyunOSSProperties.getBucketName() + "." + aliyunOSSProperties.getEndpoint().split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
在我们添加上注解后,会发现idea窗口上面出现一个红色警告:

这个警告提示是告知我们还需要引入一个依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
当我们在pom.xml文件当中配置了这项依赖之后,我们重新启动服务,大家就会看到在properties或者是yml配置文件当中,就会提示阿里云 OSS 相关的配置项。所以这项依赖它的作用就是会自动的识别被@ConfigurationProperties
注解标识的bean对象。
刚才的红色警告,已经变成了一个灰色的提示,提示我们需要重新运行springboot服务
@ConfigurationProperties注解我们已经介绍完了,接下来我们就来区分一下@ConfigurationProperties注解以及我们前面所介绍的另外一个@Value注解:
相同点:都是用来注入外部配置的属性的。
不同点:
@Value注解只能一个一个的进行外部属性的注入。
@ConfigurationProperties可以批量的将外部的属性配置注入到bean对象的属性中。
如果要注入的属性非常的多,并且还想做到复用,就可以定义这么一个bean对象。通过 configuration properties 批量的将外部的属性配置直接注入到 bin 对象的属性当中。在其他的类当中,我要想获取到注入进来的属性,我直接注入 bin 对象,然后调用 get 方法,就可以获取到对应的属性值了

总结
课堂作业
- 配置文件yml格式最注意是什么?🎤
- 为什么要将配置放到yml中保存?
- 根据上述提示优化你的工程!
3. 异常处理
前言
当前问题
当我们在修改部门数据的时候,如果输入一个在数据库表中已经存在的手机号,点击保存按钮之后,前端提示了错误信息 ,但是返回的结果并不是统一的响应结果,而是框架默认返回的错误结果 。

状态码为500,表示服务器端异常,我们打开idea,来看一下,服务器端出了什么问题。

上述错误信息的含义是,emp
员工表的phone
手机号字段的值重复了,因为在数据库表emp
中已经有了13309090001这个手机号了,我们之前设计这张表时,为phone
字段建议了唯一约束,所以该字段的值是不能重复的。
而当我们再将该员工的手机号也设置为 13309090001
,就违反了唯一约束,此时就会报错。
我们来看一下出现异常之后,最终服务端给前端响应回来的数据长什么样。

响应回来的数据是一个JSON格式的数据。但这种JSON格式的数据还是我们开发规范当中所提到的统一响应结果Result吗?
显然并不是。由于返回的数据不符合开发规范,所以前端并不能解析出响应的JSON数据 。
接下来我们需要思考的是出现异常之后,当前案例项目的异常是怎么处理的?
- 答案:没有做任何的异常处理

当我们没有做任何的异常处理时,我们三层架构处理异常的方案:
- Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。
- service 中也存在异常了,会抛给controller。
- 而在controller当中,我们也没有做任何的异常处理,所以最终异常会再往上抛。最终抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合我们的开发规范。
那么在三层构架项目中,出现了异常,该如何处理?

方案一:在所有Controller的所有方法中进行try…catch处理
- 缺点:代码臃肿(不推荐)
方案二:全局异常处理器
- 好处:简单、优雅(推荐)

全局异常处理器代码操作
全局异常处理器
我们该怎么样定义全局异常处理器?
- 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解
@RestControllerAdvice
,加上这个注解就代表我们定义了一个全局异常处理器。 - 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解
@ExceptionHandler
。通过@ExceptionHandler
注解当中的value属性
来指定我们要捕获的是哪一类型的异常。
cn.yangeit.handler
@RestControllerAdvice
public class GlobalExceptionHandler {
//处理异常
@ExceptionHandler
public Result ex(Exception e){//方法形参中指定能够处理的异常类型
e.printStackTrace();//打印堆栈中的异常信息
//捕获到异常之后,响应一个标准的Result
return Result.error("对不起,操作失败,请联系管理员");
}
}
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端
重新启动SpringBoot服务,打开浏览器,再来测试一下 修改员工 这个操作,我们依然设置已存在的 "13309090001" 这个手机号:

此时,我们可以看到,出现异常之后,异常已经被全局异常处理器捕获了。然后返回的错误信息,被前端程序正常解析,然后提示出了对应的错误提示信息。
以上就是全局异常处理器的使用,主要涉及到两个注解:
- @RestControllerAdvice //表示当前类为全局异常处理器
- @ExceptionHandler //指定可以捕获哪种类型的异常进行处理
总结
课堂作业
- 根据上述提示完成统一异常捕捉类的书写🎤
- 拓展作业:完成提示用户具体时什么错误!,如提示xxxx号码重复了!!🚀
4. 员工信息统计
员工管理的增删改查功能我们已开发完成,接下来,我们再来完成员工信息统计的接口开发。 对于这些图形报表的开发,其实呢,都是基于现成的一些图形报表的组件开发的,比如:Echarts、HighCharts等。
而报表的制作,主要是前端人员开发,引入对应的组件(比如:ECharts)即可。 服务端开发人员仅为其提供数据即可。
官网:https://echarts.apache.org/zh/index.html

4.1 职位统计
前言

对于这类的图形报表,服务端要做的,就是为其提供数据即可。 我们可以通过官方的示例,看到提供的数据其实就是X轴展示的信息,和对应的数据。

相关信息
4.1.2 接口文档
1). 基本信息
请求路径:/report/empJobData
请求方式:GET
接口描述:统计各个职位的员工人数
详细接口情况:接口文档
为了封装上面需要给前端返回的数据,在pojo包下再创建一个实体类 JobOption
,封装给前端返回的结果:
/**
* 员工职位人数统计
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JobOption {
private List jobList; //职位列表
private List dataList; //人数列表
}
代码操作
代码实现
1). 定义ReportController,并添加方法。
@Slf4j
@RequestMapping("/report")
@RestController
public class ReportController {
@Autowired
private ReportService reportService;
/**
* 统计各个职位的员工人数
*/
@GetMapping("/empJobData")
public Result getEmpJobData(){
log.info("统计各个职位的员工人数");
JobOption jobOption = reportService.getEmpJobData();
return Result.success(jobOption);
}
}
2). 定义ReportService接口,并添加接口方法。
public interface ReportService {
/**
* 统计各个职位的员工人数
* @return
*/
JobOption getEmpJobData();
}
3). 定义ReportServiceImpl实现类,并实现方法
@Service
public class ReportServiceImpl implements ReportService {
@Autowired
private EmpMapper empMapper;
@Override
public JobOption getEmpJobData() {
List<Map<String,Object>> list = empMapper.countEmpJobData();
List<Object> jobList = list.stream().map(dataMap -> dataMap.get("pos")).collect(Collectors.toList());
List<Object> dataList = list.stream().map(dataMap -> dataMap.get("total")).collect(Collectors.toList());
return new JobOption(jobList, dataList);
}
}
4). 定义EmpMapper 接口
统计的是员工的信息,所以需要操作的是员工表。 所以代码我们就写在 EmpMapper
接口中即可。
/**
* 统计各个职位的员工人数
*/
@MapKey("pos")
List<Map<String,Object>> countEmpJobData();
如果查询的记录往Map中封装,可以通过@MapKey注解指定返回的map中的唯一标识是那个字段。【也可以不指定】
5). 定义EmpMapper.xml
<!-- 统计各个职位的员工人数 -->
<select id="countEmpJobData" resultType="java.util.Map">
select
(case job when 1 then '班主任'
when 2 then '讲师'
when 3 then '学工主管'
when 4 then '教研主管'
when 5 then '咨询师'
else '其他' end) pos,
count(*) total
from emp group by job
order by total
</select>
case流程控制函数:
语法一:case when cond1 then res1 [ when cond2 then res2 ] else res end ;
- 含义:如果 cond1 成立, 取 res1。 如果 cond2 成立,取 res2。 如果前面的条件都不成立,则取 res。
语法二(仅适用于等值匹配):case expr when val1 then res1 [ when val2 then res2 ] else res end ;
- 含义:如果 expr 的值为 val1 , 取 res1。 如果 expr 的值为 val2 ,取 res2。 如果前面的条件都不成立,则取 res。
Apifox测试
重新启动服务,打开Apifox进行测试。

联调测试

总结
课堂作业
- 根据提示,完成代码🎤
4.2 性别统计
性别统计

对于这类的图形报表,服务端要做的,就是为其提供数据即可。 我们可以通过官方的示例,看到提供的数据就是一个json格式的数据。

代码操作
代码实现
1). 在ReportController,添加方法。
/**
* 统计员工性别信息
*/
@GetMapping("/empGenderData")
public Result getEmpGenderData(){
log.info("统计员工性别信息");
List<Map> genderList = reportService.getEmpGenderData();
return Result.success(genderList);
}
2). 在ReportService接口,添加接口方法。
/**
* 统计员工性别信息
*/
List<Map> getEmpGenderData();
3). 在ReportServiceImpl实现类,实现方法
@Override
public List<Map> getEmpGenderData() {
return empMapper.countEmpGenderData();
}
4). 定义EmpMapper 接口
统计的是员工的信息,所以需要操作的是员工表。 所以代码我们就写在 EmpMapper
接口中即可。
/**
* 统计员工性别信息
*/
@MapKey("name")
List<Map> countEmpGenderData();
5). 定义EmpMapper.xml
<!-- 统计员工的性别信息 -->
<select id="countEmpGenderData" resultType="java.util.Map">
select
if(gender = 1, '男', '女') as name,
count(*) as value
from emp group by gender ;
</select>
if函数语法:
if(条件, 条件为true取值, 条件为false取值)
ifnull函数语法:
ifnull(expr, val1)
如果expr不为null,取自身,否则取val1
Apifox测试

联调测试
