创建一、Springboot 项目的搭建
1)创建一个空项目(空项目起手比较干净)2)完成增删改查的基本依赖 start、lombok、mysql、mybatis ,其他的依赖用哪个添加哪个
<!-- 一定有一个parent父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.6</version>
</parent>
<dependencies>
<!-- 跟spring环境有关系的依赖:start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 生成set get方法的依赖:lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 跟mysql有关系的依赖: -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 跟mybatis有关系的依赖 mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
</dependencies>
3)准备基础包结构,并引入实体类及统一的响应结果封装类Result ,准备基础包结构
4)连接数据库
配置连接信息 application.yml
# 应用服务 WEB 访问端口
server:
port: 8080
#下面这些内容是为了让MyBatis映射
#指定Mybatis的Mapper文件
mybatis:
mapper-locations: classpath:mappers/*xml
type-aliases-package: com.itheima.mybatis.entity #指定Mybatis的实体目录
#mybatis
configuration:
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 日志输出
#开启驼峰命名映射开关
map-underscore-to-camel-case: true
# 配置数据库的链接信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/tlias
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 1234
# spring:需要上传大文件的配置 不配置的话默认为1M
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
#阿里云OSS
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
bucketName: java-426-ai
region: cn-beijing
连接数据库
基础功能 三层架构:基本的增删改查
准备基础代码结构 完成基本的增删改查
TliasApplication项目启动类
@SpringBootApplication
@MapperScan("com.itheima.mapper")
public class TliasApplication {
public static void main(String[] args) {
SpringApplication.run(TliasApplication.class, args);
}
}
DeptController
/**
* 控制层: 负责接收前端发起的请求,并调用service查询部门数据,然后响应结果。
*/
@RequestMapping("/depts") //抽取请求地址重复的部分
@RestController
public class DeptController {
//获取业务层对象
@Autowired //从ioc 容器中找DeptService类型的bean对象
private DeptService deptService;
/**
* 1.查询部门列表
*/
@GetMapping
public Result list(){
List<Dept> deptList = deptService.findAll();
return Result.success(deptList);
}
/**
* 2.删除部门数据请求
* @param id
* @return
*/
@DeleteMapping //除了查询有,增删改都没有返回值,返回执行成功的Success信息即可
public Result deleteDept(Integer id) { //根据id删除对应数据,需要有参数id
deptService.deleteDept(id);
return Result.success();
}
/**
* 3.新增部门数据
* @param dept
*/
@PostMapping //@RestController不是已经包含了@RequestBody吗?为什么加
public Result insertDept(@RequestBody Dept dept) {
deptService.insertDept(dept);
return Result.success();
}
/**
* 4.根据id查询部门
* 数据回显请求
* @return dept
*/
@GetMapping("/{id}") //再看一遍@PathVariable 注解用于将 URL 中的路径变量绑定到控制器方法的参数上,方便从请求 URL 里提取动态数据以进行后续处理。
public Result selectDeptById(@PathVariable Integer id) {
Dept dept = deptService.selectDeptById(id);
return Result.success(dept);
}
/**
* 5.修改数据
* @param dept
* @return
*/
@PutMapping
public Result updateDept(@RequestBody Dept dept) {
deptService.updateDept(dept);
return Result.success();
}
}
DeptService
public interface DeptService {
/**
* 业务: 查询所有数据
* @return 返回List<Dept>对象集合
*/
List<Dept> findAll();
/**
* 业务: 根据id删除对应的数据
* @param id
*/
void deleteDept(Integer id);
/**
* 业务: 新增部门数据
* @param dept
*/
void insertDept(Dept dept);
/**
* 业务: 根据id查询部门(数据回显)业务
* @param id
* @return
*/
Dept selectDeptById(Integer id);
/**
* 业务: 根据修改部门业务
* @param dept
*/
void updateDept(Dept dept);
}
DeptServiceImpl
//业务层(服务层): 负责处理业务逻辑,实现业务(接口)的功能,并返回数据给控制层
/**
* @Service 介绍: 是 Spring 框架注解,用于将类标记为服务层组件,
* 作用: 使 Spring 容器自动扫描并将其注册为 Bean 以处理业务逻辑。(把类的对象交给ioc)
*/
@Service
public class DeptServiceImpl implements DeptService {
@Autowired //从ioc 容器中找DeptMapper类型的bean对象
private DeptMapper deptMapper;
/**
* 业务:查询部门列表
* @return
*/
@Override
public List<Dept> findAll() {
//业务层向持久层要返回的对象集合 List<Dept>,然后把对象集合返回给控制层
List<Dept> deptList = deptMapper.findAll();
return deptList;
}
/**
* 业务: 根据id删除对应的数据
* @param id
*/
@Override
public void deleteDept(Integer id) {
//判断这个部门下是否有员工,select count(*) from emp where dept_id?
// Integer count = empMapper.selectEmpNumByDeptID(id);
// if (count > 0){
// throw new TliasException("对不起,当前部门下有员工,不能直接删除");
// }
deptMapper.deleteDept(id);
}
/**
* 业务: 新增部门数据
* @param dept
*/
@Override
public void insertDept(Dept dept) {
//给对象补全属性 为什么给对象补全?只有名字吗
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.insertDept(dept);
}
/**
* 业务: 根据id查询部门(数据回显)业务
* @param id
* @return
*/
@Override
public Dept selectDeptById(Integer id) {
Dept dept = deptMapper.selectDeptById(id);
return dept;
}
/**
* 业务: 根据修改部门业务
* @param dept
*/
@Override
public void updateDept(Dept dept) {
//给对象补全属性
dept.setUpdateTime(LocalDateTime.now());
deptMapper.updateDept(dept);
}
}
DeptMapper
//持久层: 和数据库打交道,负责用sql控制数据库中的数据,并返回数据给业务层
/**
* @Mapper 是 MyBatis 框架中用于标记接口为 MyBatis 映射器的标签
* 使 Spring 能自动扫描并将该接口注册为 Bean,以便使用其定义的方法进行数据库操作。
*/
@Mapper
public interface DeptMapper {
/**
* 通过sql语句查询所有部门
* @return List<Dept>部门集合
*/
@Select("select * from dept")
List<Dept> findAll();
/**
* 通过sql语句根据id删除对应部门
* @Delete("") 删除注解,
* 注意:
* 1. !!! 字段 = #{成员变量}
* 2.!!! @Param("")
* 需要向 SQL 映射语句传递多个参数时容易出错,
* @Param 注解可以为每个参数指定一个有意义的名称,使 SQL 映射文件中的参数引用更加清晰。
*/
@Delete("delete from dept where id = #{id}")
void deleteDept(@Param("id") Integer id);
/**
* 通过sql语句新增部门
* @param dept
*/
@Insert("insert into dept (name,create_time,update_time) value(#{name},#{createTime},#{updateTime})")
void insertDept(Dept dept);
/**
* 通过sql语句根据ID查询部门回显和修改部门
* @param id
* @return
*/
@Select("select * from dept where id = #{id}")
Dept selectDeptById(@Param("id") Integer id);
/**
* 通过sql语句修改部门
* @param dept
*/
@Update("update dept set name = #{name},update_time = #{updateTime} where id = #{id}")
void updateDept(Dept dept);
}
功能二、分页功能
Emp增删改查功能代码类似,不用的是多了多表查询(这块重点是SQL 语句)
1)实体类Emp、EmpExpr 分页查询返回的封装 PageResult<T> 、EmpQueryParam
@Data //员工表1
public class EmpExpr {
private Integer id; //ID
private Integer empId; //员工ID
private LocalDate begin; //开始时间
private LocalDate end; //结束时间
private String company; //公司名称
private String job; //职位
}
@Data //工作经历表2
public class EmpExpr {
private Integer id; //ID
private Integer empId; //员工ID
private LocalDate begin; //开始时间
private LocalDate end; //结束时间
private String company; //公司名称
private String job; //职位
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T>{
private Long total; // 总记录数
private List<T> rows; // 当前页的数据
}
@Data
public class EmpQueryParam {
private Integer page = 1; //页码
private Integer pageSize = 10; //每页展示记录数
private String name; //姓名
private Integer gender; //性别
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate begin; //入职开始时间
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate end; //入职结束时间
}
2)Emp员工管理的基础结构,包括Controller、Service、ServiceImpl、Mapper、Mapper.xml
分页功能涉及 分页、模糊查询、多表查询:引入分页插件依赖-三层架构-SQL模糊和条件查询
引入分页插件依赖
<!-- 分页插件PageHelper --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>
@RestController
public class EmpController {
@Autowired
private EmpService empService;
/**
* 1、分页查询请求
*
* @param empQueryParam
* @return
*/
@GetMapping("/emps")
public Result selectEmp(EmpQueryParam empQueryParam) {
//调用业务层方法,把页码和每页数据量传递过去
PageResult<Emp> pageResult = empService.selectEmp(empQueryParam);
return Result.success(pageResult);
}
/**
* 2、查询所有员工信息
*
* @return
*/
@GetMapping("/emps/list") //注意路径
public Result selectEmpAll() {
List<Emp> empList = empService.selectEmpAll();
return Result.success(empList);
}
/**
* 3、添加员工请求
*
* @param emp
* @return
*/
@PostMapping("/emps")
public Result save(@RequestBody Emp emp) throws Exception {
empService.save(emp);
return Result.success();
}
/**
* 4、批量删除 员工请求
*
* @param ids
* @return
*/
@DeleteMapping("/emps")
public Result deleteEmp(@RequestParam("ids") List<Integer> ids) { //数组 集合都行
empService.deleteEmp(ids);
return Result.success();
}
/**
* 5、修改员工信息 时的根据id数据回显(映射回显)的请求
*
* @param id
* @return
*/
@GetMapping("/emps/{id}")
public Result getByID(@PathVariable("id") Integer id) {
Emp emp = empService.getEmpWithExprById(id);
return Result.success(emp);
}
/**
* 6、修改员工信息:先删除后新增
* @param emp
* @return
* @throws Exception
*/
@PutMapping("/emps")
public Result updateEmp(@RequestBody Emp emp) throws Exception {
empService.updateEmp(emp);
return Result.success();
}
}
EmpService
//业务层:
public interface EmpService {
/**
* 分页查询的业务功能
*
* @param empQueryParam
* @return
*/
PageResult<Emp> selectEmp(EmpQueryParam empQueryParam);
/**
* 查询
* @return
*/
List<Emp> selectEmpAll();
/**
* 添加员工的业务功能
*
* @param emp
*/
void save(Emp emp) throws Exception;
/**
* 批量删除员工的业务功能
*
* @param ids
*/
void deleteEmp(List<Integer> ids);
/**
* 修改员工信息业务功能-回显:
* 一次性把员工基本信息和员工工作经历都查询并回显出来
*
* @param id
* @return
*/
Emp getEmpWithExprById(Integer id);
/**
* 修改员工信息-修改:
* 修改的逻辑->先删除后新增
* @param emp
*/
void updateEmp(Emp emp);
/**
* 登录功能
* @param emp
* @return
*/
LoginInfo login(Emp emp);
}
EmpServiceImpl
//业务层:
@Service
public class EmpServiceImpl implements EmpService {
//从ioc容器中找到持久层层对象
@Autowired
private EmpMapper empMapper;
@Autowired
private EmpExprMapper empExprMapper;
@Autowired
private EmpLogService empLogService;
/**
* 分页查询的业务功能
*
* @param empQueryParam
* @return
*/
@Override
public PageResult<Emp> selectEmp(EmpQueryParam empQueryParam) {
//三步固定写法使用分页插件(使用前需要导入插件依赖 PageHelper)
//1.设置分页参数
PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize());
//2.调用查询所有的方法
List<Emp> empList = empMapper.selectAll(empQueryParam);
//3.把查询所有数据的集合强转成 Page 对象
Page p = (Page) empList;
//创建一个分页查询的数据封装对象(固定写法) //?什么是泛型
PageResult<Emp> pageResult = new PageResult<>();
pageResult.setTotal(p.getTotal());
pageResult.setRows(p.getResult());
//封装好的数据返回给控制层
return pageResult;
}
/**
* 查询
*
* @return
*/
@Override
public List<Emp> selectEmpAll() {
List<Emp> empList = empMapper.selectAll(new EmpQueryParam());
return empList;
}
/**
* 添加员工的业务功能
*
* @param emp
*/
//点一事务管理: 一组操作事务的集合,要么同时成功要么同时失败
//分析: 转账一方转账出去,手账一方没收到就会回滚,退回给转账方
//将当前方法交给spring进行事务管理,通过spring事务管理注解@Transactional控制业务层方法的事务
//个人理解:添加该注解后进行事务管理,员工信息和员工经历一起添加成功或者失败弥补了之前程序的漏洞 (为什么用事务管理功能)
//@Transactional
//点二事务管理细节:
//默认出现RuntimeException(运行时异常)才会回滚事务。
//让所有的异常都回滚,配置@Transactional 注解属性rollbackFor即可实现 记住★★★
@Transactional(rollbackFor = Exception.class)
@Override
public void save(Emp emp) {
try {
//1.补全基础属性
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
emp.setPassword("123456");
//2.保存员工基本信息
empMapper.insert(emp);
//int i = 1 / 0;
// 检验事务管理是否开启,员工信息添加成功 员工经历添加失败 则都添加失败
//if (true) {throw new Exception("出异常啦.....");}
//3.保存员工的工作经历 - 批量
Integer empId = emp.getId();
List<EmpExpr> exprList = emp.getExprList();
if (!CollectionUtils.isEmpty(exprList)) {
exprList.forEach(empExpr -> empExpr.setEmpId(empId));
empExprMapper.insertBatch(exprList);
}
} finally {
//记录操作日志
EmpLog empLog = new EmpLog(null, LocalDateTime.now(), emp.toString());
empLogService.insertLog(empLog);
}
}
/**
* 批量删除员工的业务功能
*
* @param ids
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void deleteEmp(List<Integer> ids) {
//删除员工基本信息
empMapper.deleteByIds(ids);
//删除员工工作经历
empExprMapper.deleteByEmpIds(ids);
}
/**
* 修改员工信息:
* 一次性把员工基本信息和员工工作经历都查询出来
*
* @param id
* @return
*/
@Override
public Emp getEmpWithExprById(Integer id) {
return empMapper.getEmpWithEmpExprById(id);
}
// @Override
// public Emp getEmpById(Integer id) {
// //回显的简单写法: 执行两个SQL
// //查询员工基本信息
// Emp emp = empMapper.getById(id);
// //查询此员工的工作经历
// List<EmpExpr> exprList = empExprMapper.selectByEmpId(id);
// return emp;
// }
/**
* 修改员工信息:
* 修改的逻辑->先删除后新增
*
* @param emp
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void updateEmp(Emp emp) {
//1.更新员工的基本信息
emp.setUpdateTime(LocalDateTime.now());
empMapper.updateById(emp);
//2.删除此员工的工作经历
Integer id = emp.getId();
empExprMapper.deleteByEmpIds(Arrays.asList(id));
//3.新增此员工的工作经历
if (!CollectionUtils.isEmpty(emp.getExprList())) {
emp.getExprList().forEach(empExpr -> empExpr.setEmpId(id));
empExprMapper.insertBatch(emp.getExprList());
}
}
/**
* 登录功能
*
* @param emp
* @return
*/
@Override
public LoginInfo login(Emp emp) {
Emp emp1 = empMapper.selectByUsernameAndPassword(emp.getUsername(), emp.getPassword());
if (emp1 != null) {
LoginInfo loginInfo = new LoginInfo();
loginInfo.setId(emp1.getId());
loginInfo.setUsername(emp1.getUsername());
loginInfo.setName(emp1.getName());
//loginInfo.setToken();// TODD 先不处理
//这会儿处理->使用JWT工具类生成令牌token
Map<String, Object> claims = new HashMap<>();//这个map不能放用户的敏感信息 password
claims.put("id", emp1.getId());
claims.put("username", emp1.getUsername());
String token = JwtUtils.generateJwt(claims);
loginInfo.setToken(token);
return loginInfo;
}
return null;
}
}
EmpMapper
@Mapper
public interface EmpMapper {
/**
* 查询员工列表以及分页查询
* @param empQueryParam
* @return
*/
List<Emp> selectAll(EmpQueryParam empQueryParam);
/**
* 新增员工数据
* @param emp
*/
//Mybatis的主键返回功能标签@Options: 保存工作经历后获取员工的ID 完成对应员工工作经历的新增
@Options(useGeneratedKeys = true,keyProperty = "id")
@Insert("insert into emp(username,password, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time)" +
"values (#{username},#{password},#{name},#{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})")
void insert(Emp emp);
/**
* 删除员工数据
* @param ids
*/
void deleteByIds(@Param("ids") List<Integer> ids);
//映射回显
Emp getEmpWithEmpExprById(@Param("id") Integer id);
Emp selectEmpById(@Param("id") Integer id);
//修改员工数据
void updateById(Emp emp);
//查询部门人数
Integer selectEmpNumByDeptID(@Param("deptId") Integer deptId);
}
EmpExprMapper
@Mapper
public interface EmpExprMapper {
/**
* 批量插入员工工作经历数据
* @param exprList
*/
void insertBatch(@Param("exprList") List<EmpExpr> exprList);
void deleteByEmpIds(@Param("ids") List<Integer> ids);
}
EmpMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
namespace属性的值 : 给的是接口的全限定名 (包名.接口名)
-->
<mapper namespace="com.itheima.mapper.EmpMapper">
<!--
id的属性值: 绑定的是方法名
-->
<delete id="deleteByIds">
delete from emp where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
<!-- 修改员工信息 -->
<update id="updateById">
update emp
<!-- 优化修改功能: 单个修改时更加灵活更有扩展性 -->
<!-- set标签的作用是: 消除sql语句中的, -->
<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="salary != null">salary=#{salary},</if>
<if test="job != null">job=#{job},</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>
<!-- 根据id查询员工信息(回显) -->
<select id="selectEmpById" resultType="com.itheima.pojo.Emp">
select * from emp where id = #{id};
</select>
<!-- 查询员工基本信息和员工经历 -->
<select id="selectAll" resultType="com.itheima.pojo.Emp">
select emp.*,dept.name as deptName
from emp
left join dept on emp.dept_id = dept.id
<where>
<if test="name!=null and name!=''">
emp.name like concat('%',#{name},'%')
</if>
<if test="gender!=null">
and emp.gender = #{gender}
</if>
<if test="begin!=null and end!=null">
and emp.entry_date between #{begin} and #{end}
</if>
</where>
</select>
<resultMap id="baseEmpMap" type="com.itheima.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"/>
<collection property="exprList" ofType="com.itheima.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>
<select id="getEmpWithEmpExprById" resultMap="baseEmpMap">
SELECT e.*,
ee.id ee_id,
ee.emp_id ee_emp_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 ee.emp_id = e.id
WHERE e.id = #{id}
</select>
<!-- 统计各个职位的员工人数 -->
<select id="getEmpJobData" resultType="map">
select
case job when 1 then '班主任'
when 2 then '讲师'
when 3 then '学工主管'
when 4 then '教研主管'
when 5 then '咨询师'
else '其他' end job
,count(*) total from emp group by job order by total desc
</select>
<!-- 统计员工的性别信息 -->
<select id="getEmpGenderData" resultType="java.util.Map">
select
if(gender = 1,'男','女') as name,
count(*) as value
from emp group by gender;
</select>
<!-- 删除部门判断员工是否为空 -->
<select id="selectEmpNumByDeptID" resultType="java.lang.Integer">
select count(0)
from emp
where dept_id = #{deptId};
</select>
</mapper>
EmpExprMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
namespace属性的值 : 给的是接口的全限定名 (包名.接口名)
-->
<mapper namespace="com.itheima.mapper.EmpExprMapper">
<!--
id的属性值: 绑定的是方法名
-->
<!-- 批量插入员工工作经历信息 -->
<insert id="insertBatch">
insert into emp_expr (emp_id,begin,end,company,job) values
<foreach collection="exprList" item="expr" separator=",">
(#{expr.empId},#{expr.begin},#{expr.end},#{expr.company},#{expr.job})
</foreach>
</insert>
<delete id="deleteByEmpIds">
delete from emp_expr where emp_id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
功能三、分多表操作涉及事务管理
事务管理问题分析:
目前我们实现的新增员工功能中,操作了两次数据库,执行了两次 insert 操作。
第一次:保存员工的基本信息到 emp 表中。
第二次:保存员工的工作经历信息到 emp_expr 表中。
我们看到,员工表 emp 数据保存成功了, 但是 emp_expr 员工工作经历信息表,数据保存失败了。 那是否允许这种情况发生呢?不允许
因为这属于一个业务操作,如果保存员工信息成功了,保存工作经历信息失败了,就会造成数据库数据的不完整、不一致。
那如何解决这个问题呢? 这需要通过数据库中的事务来解决这个问题。
事务是一组操作的集合,要么同时成功,要么同时失败
1)开启事务加一个@Transactional注解就行了:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
添加删除修改ServiceImpl业务层都加上@Transactional
/** * 3、添加员工的业务功能 * * @param emp */ @Transactional(rollbackFor = Exception.class) @Override public void save(Emp emp) { /** * 4、批量删除员工的业务功能 * * @param ids */ @Transactional(rollbackFor = Exception.class) /** * 6、修改员工信息:修改的逻辑->先删除后新增 * * @param emp */ @Override @Transactional(rollbackFor = Exception.class)
#spring事务管理日志配置
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
面试题:事务有哪些特性?
原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败。
一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
功能四、上传图片
准备阿里云OSS
1)开通OSS-创建一个Bucket-配置AccessKey(AccessKey保存到本地环境变量)
2)引入依赖
<!--阿里云OSS依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
3)AliyunOSSOperator配置阿里云oss的类 和读取类
@Component
public class AliyunOSSOperator {
@Autowired
private OperatorPropertie operatorPropertie;
//修改一:改成自己的OSS服务器中的bucket对应的域名中
//private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
//修改二:改成自己的OSS服务器中的bucket名称
//private String bucketName = "java-426-ai";
//修改三:改成自己的OSS服务器所属区域
//private String region = "cn-beijing";
/*优化
为了方便统一管理 配置少的时候可以用@Value来读取配置文件中的值
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
@Value("${aliyun.oss.region}")
private String region;
*/
/* 配置多的时候用@ConfigurationProperties注解
需要单独来一个配置类,然后使用get方法获取配置文件中的值
*/
public String upload(byte[] content, String originalFilename) throws Exception {
String endpoint = operatorPropertie.getEndpoint();
String bucketName = operatorPropertie.getBucketName();
String region = operatorPropertie.getRegion();
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Object完整路径,例如202406/1.png。Object完整路径中不能包含Bucket名称。
//获取当前系统日期的字符串,格式为 yyyy/MM
String dir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM")); //2025/04
//生成一个新的不重复的文件名
String newFileName = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = dir + "/" + newFileName;
// 创建OSSClient实例。
ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
OSS ossClient = OSSClientBuilder.create()
.endpoint(endpoint)
.credentialsProvider(credentialsProvider)
.clientConfiguration(clientBuilderConfiguration)
.region(region)
.build();
try {
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content));
} finally {
ossClient.shutdown();
}
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
}
}
@Data
@ConfigurationProperties(prefix = "aliyun.oss")//prefix 前缀
@Component
//@ConfigurationProperties 读取方式
public class OperatorPropertie {
private String endpoint;
private String bucketName;
private String region;
}
4)上传文件的UploadController、UploadService、UploadServiceImpl
@RestController
public class UploadController {
@Autowired
private UploadService uploadService;
/**
* 上传文件
*/
@PostMapping("/upload")
public Result upload(MultipartFile file) throws Exception {
String url = uploadService.upload(file);
return Result.success(url);
}
}
public interface UploadService {
String upload(MultipartFile file) throws Exception;
}
@Service
public class UploadServiceImpl implements UploadService {
@Autowired
private AliyunOSSOperator aliyunOSSOperator;
@Override
public String upload(MultipartFile file) throws Exception {
String url = aliyunOSSOperator.upload(file.getBytes(), file.getOriginalFilename());
return url;
}
}
5)阿里云Oss和上传文件的大小配置
# 配置数据库的链接信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/tlias
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 1234
# spring:需要上传大文件的配置 不配置的话默认为1M
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
#阿里云OSS
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
bucketName: java-426-ai
region: cn-beijing
功能五、全局异常处理
问题分析
当我们在修改部门数据的时候,如果输入一个在数据库表中已经存在的手机号,点击保存按钮之后,前端提示了错误信息,但是返回的结果并不是统一的响应结果,而是框架默认返回的错误结果 。
状态码为500,表示服务器端异常,我们打开idea,来看一下,服务器端出了什么问题。
上述错误信息的含义是,emp
员工表的phone
手机号字段的值重复了,因为在数据库表emp
中已经有了13309090027
这个手机号了,我们之前设计这张表时,为phone
字段建议了唯一约束,所以该字段的值是不能重复的。
而当我们再将该员工的手机号也设置为 13309090027
,就违反了唯一约束,此时就会报错。
我们来看一下出现异常之后,最终服务端给前端响应回来的数据长什么样。
响应回来的数据是一个JSON格式的数据。但这种JSON格式的数据还是我们开发规范当中所提到的统一响应结果Result吗?显然并不是。由于返回的数据不符合开发规范,所以前端并不能解析出响应的JSON数据 。
接下来我们需要思考的是出现异常之后,当前案例项目的异常是怎么处理的? 答案:没有做任何的异常处理
那么在三层构架项目中,出现了异常,该如何处理?
方案:全局异常处理器
我们该怎么样定义全局异常处理器?
1)定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解@RestControllerAdvice,加上这个注解就代表我们定义了一个全局异常处理器。
2)在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解@ExceptionHandler。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。
GlobalExceptionAdvice
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler(TliasException.class)
public Result exceptionHandler(TliasException e) {//处理自定义异常
e.printStackTrace();
return Result.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result allException(Exception e){ //所有异常
e.printStackTrace(); //报错打印到后台
return Result.error("服务器异常,请联系管理员"); //Result统一数据格式
}
@ExceptionHandler(DuplicateKeyException.class)
public Result allException(DuplicateKeyException e){ //有重复值的异常
e.printStackTrace(); //报错打印到后台
if (e.getMessage().contains("emp.username")){
return Result.error("您新增的用户名已存在");
}
if (e.getMessage().contains("emp.phone")){
return Result.error("对应手机号的用户已存在");
}
if (e.getMessage().contains("emp.image")){
return Result.error("对应手已存在");
}
return Result.error("数据已存在"); //Result统一数据格式
}
}
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端
重新启动SpringBoot服务,打开浏览器,再来测试一下 修改员工 这个操作,我们依然设置已存在的 13309090027
这个手机号:
此时,我们可以看到,出现异常之后,异常已经被全局异常处理器捕获了。然后返回的错误信息,被前端程序正常解析,然后提示出了对应的错误提示信息。
以上就是全局异常处理器的使用,主要涉及到两个注解:
@RestControllerAdvice //表示当前类为全局异常处理器
@ExceptionHandler //指定可以捕获哪种类型的异常进行处理
功能六、基本的登录认证
先上图看实现效果:
需求:
用户名、密码都正确才可以登录成功访问系统,没有登录的话阻止直接访问和使用系统
一、登录功能
1). 准备实体类 LoginInfo
, 封装登录成功后, 返回给前端的数据 。
/**
* 封装登录结果
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
private Integer id; //ID
private String username; //用户名
private String name; //姓名
private String token; //令牌
}
2). 定义LoginController
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmployeeService employeeService;
/**
* 登录
*/
@PostMapping("/login")
public Result login(@RequestBody Employee employee){
log.info("员工进行登录了..... ");
LoginInfo loginInfo = employeeService.login(employee);
if(loginInfo != null){
return Result.success(loginInfo);
}
return Result.error("用户名或密码错误!");
}
/**
* 空请求[不用管, 也不要动]
*/
@GetMapping("/index")
public Result index(){
return Result.success();
}
}
3)
EmployeeService
接口中增加 login 登录方法
public interface EmployeeService {
/**
* 登录
*/
LoginInfo login(Employee employee);
4)
EmployeeServiceImpl
接口实现类中增加 login 登录方法
@Slf4j
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Override
public LoginInfo login(Employee employee) {
//1. 调用mapper方法查询数据库
Employee e = employeeMapper.selectByUsernameAndPass(employee);
//2. 判断
if (e != null) { //登录成功 - 生成JWT令牌
Map<String,Object> claims = new HashMap<>();
claims.put("id", e.getId());
claims.put("username", e.getUsername());
claims.put("name", e.getName());
String jwt = JwtUtils.generateJwt(claims);
return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt);
}
return null;
}
}
5)
EmployeeMapper
增加接口方法
@Mapper
public interface EmployeeMapper {
/**
* 根据用户名查询用户
*/
@Select("select * from tb_employee where username = #{username} and password = #{password} and status = 1")
Employee selectByUsernameAndPass(Employee employee);
}
效果如下:
登录校验功能(会话技术)
什么是登录校验?
所谓登录校验,指的是我们在服务器端接收到浏览器发送过来的请求之后,首先我们要对请求进行校验。先要校验一下用户登录了没有,如果用户已经登录了,就直接执行对应的业务操作就可以了;如果用户没有登录,此时就不允许他执行相关的业务操作,直接给前端响应一个错误的结果,最终跳转到登录页面,要求他登录成功之后,再来访问对应的数据。
主流用JWT令牌技术进行会话跟踪
在JWT登录认证的场景中我们发现,整个流程当中涉及到两步操作:
- 在登录成功之后,要生成令牌。
- 每一次请求当中,要接收令牌并对令牌进行校验。
首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
1)引入JWT工具类:在项目工程下创建 com.itheima.util 包,并把提供JWT工具类复制到该包下
public class JwtUtils {
private static String signKey = "SVRIRUlNQQ==";
private static Long expire = 43200000L;
/**
* 生成JWT令牌
* @return
*/
public static String generateJwt(Map<String,Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
2)完善EmployeeServiceImpl
接口实现类中 login 登录方法的逻辑,生成令牌并返回
@Slf4j
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Override
public LoginInfo login(Employee employee) {
//1. 调用mapper方法查询数据库
Employee e = employeeMapper.selectByUsernameAndPass(employee);
//2. 判断
if (e != null) { //登录成功 - 生成JWT令牌
Map<String,Object> claims = new HashMap<>();
claims.put("id", e.getId());
claims.put("username", e.getUsername());
claims.put("name", e.getName());
String jwt = JwtUtils.generateJwt(claims);
return new LoginInfo(e.getId(), e.getUsername(), e.getName(), jwt);
}
return null;
}
登录请求完成后,可以看到JWT令牌已经响应给了前端,此时前端就会将JWT令牌存储在浏览器本地。
登录校验过滤器功能的实现(统一拦截技术)
在后续的请求当中,都会在请求头中携带JWT令牌到服务端,而服务端需要统一拦截所有的请求,从而判断是否携带的有合法的JWT令牌。那怎么样来统一拦截到所有的请求校验令牌的有效性呢?我们有两种解决方案:
- Filter过滤器
- Interceptor拦截器
这里先用Filter过滤器,还是先实现效果
1). 定义过滤器-配置过滤器
@Slf4j
@WebFilter(urlPatterns = "/*") //配置过滤器要拦截的请求路径( /* 表示拦截浏览器的所有请求 )
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
//1. 获取请求url。
String url = request.getRequestURL().toString();
//2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){ //登录请求
log.info("登录请求 , 直接放行");
chain.doFilter(request, response);
return;
}
//3. 获取请求头中的令牌(token)。
String jwt = request.getHeader("token");
//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){ //jwt为空
log.info("获取到jwt令牌为空, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return;
}
//5. 解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败, 返回错误结果401");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return;
}
//6. 放行。
log.info("令牌合法, 放行");
chain.doFilter(request , response);
}
}
/**
* 启动类
*/
@SpringBootApplication
@ServletComponentScan //使用过滤器,别忘了开启对Servlet组件的支持
public class ShopAdminApplication {
public static void main(String[] args) {
SpringApplication.run(ShopAdminApplication.class, args);
}
}
最终要实现的效果:如果员工没有登录的情况下,拿着功能地址访问系统,自动跳转到登录页面。
功能六、日志管理技术
功能七、员工性别和员工职位统计
员工性别统计
需求
对于这类的图形报表,服务端要做的,就是为其提供数据即可。 我们可以通过官方的示例,看到提供的数据就是一个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测试
联调测试
员工职位统计
对于这类的图形报表,服务端要做的,就是为其提供数据即可。 我们可以通过官方的示例,看到提供的数据其实就是X轴展示的信息,和对应的数据。
参照资料中提供的接口文档,查看 数据统计
-> 员工职位统计
接口的描述。
代码实现:
1)定义封装结果对象JobOption
在 com.itheima.pojo 包中定义实体类 JobOption
package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JobOption {
private List jobList;
private List dataList;
}
2). 定义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);
}
}
3). 定义ReportService接口,并添加接口方法。
public interface ReportService {
/**
* 统计各个职位的员工人数
* @return
*/
JobOption getEmpJobData();
}
4). 定义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")).toList();
List<Object> dataList = list.stream().map(dataMap -> dataMap.get("total")).toList();
return new JobOption(jobList, dataList);
}
}
5). 定义EmpMapper 接口
/**
* 统计各个职位的员工人数
*/
@MapKey("pos")
List<Map<String,Object>> countEmpJobData();
//如果查询的记录往Map中封装,可以通过@MapKey注解指定返回的map中的唯一标识是那个字段。【也可以不指定】
6). 定义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进行测试。
联调测试
文章有(0)条网友点评