本次案例模拟公司后端人员开发场景:
前端人员已经帮我们开发好了
,我们只需要关注服务端接口的开发。-- 部门管理
create table dept(
id int unsigned primary key auto_increment comment '主键ID',
name varchar(10) not null unique comment '部门名称',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '部门表';
insert into dept (id, name, create_time, update_time) values(1,'学工部',now(),now()),(2,'教研部',now(),now()),(3,'咨询部',now(),now()), (4,'就业部',now(),now()),(5,'人事部',now(),now());
-- 员工管理(带约束)
create table emp (
id int unsigned primary key auto_increment comment 'ID',
username varchar(20) not null unique comment '用户名',
password varchar(32) default '123456' comment '密码',
name varchar(10) not null comment '姓名',
gender tinyint unsigned not null comment '性别, 说明: 1 男, 2 女',
image varchar(300) comment '图像',
job tinyint unsigned comment '职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师',
entrydate date comment '入职时间',
dept_id int unsigned comment '部门ID',
create_time datetime not nul`dept``emp`l comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '员工表';
INSERT INTO emp
(id, username, password, name, gender, image, job, entrydate,dept_id, create_time, update_time) VALUES
(1,'jinyong','123456','金庸',1,'1.jpg',4,'2000-01-01',2,now(),now()),
(2,'zhangwuji','123456','张无忌',1,'2.jpg',2,'2015-01-01',2,now(),now()),
(3,'yangxiao','123456','杨逍',1,'3.jpg',2,'2008-05-01',2,now(),now()),
(4,'weiyixiao','123456','韦一笑',1,'4.jpg',2,'2007-01-01',2,now(),now()),
(5,'changyuchun','123456','常遇春',1,'5.jpg',2,'2012-12-05',2,now(),now()),
(6,'xiaozhao','123456','小昭',2,'6.jpg',3,'2013-09-05',1,now(),now()),
(7,'jixiaofu','123456','纪晓芙',2,'7.jpg',1,'2005-08-01',1,now(),now()),
(8,'zhouzhiruo','123456','周芷若',2,'8.jpg',1,'2014-11-09',1,now(),now()),
(9,'dingminjun','123456','丁敏君',2,'9.jpg',1,'2011-03-11',1,now(),now()),
(10,'zhaomin','123456','赵敏',2,'10.jpg',1,'2013-09-05',1,now(),now()),
(11,'luzhangke','123456','鹿杖客',1,'11.jpg',5,'2007-02-01',3,now(),now()),
(12,'hebiweng','123456','鹤笔翁',1,'12.jpg',5,'2008-08-18',3,now(),now()),
(13,'fangdongbai','123456','方东白',1,'13.jpg',5,'2012-11-01',3,now(),now()),
(14,'zhangsanfeng','123456','张三丰',1,'14.jpg',2,'2002-08-01',2,now(),now()),
(15,'yulianzhou','123456','俞莲舟',1,'15.jpg',2,'2011-05-01',2,now(),now()),
(16,'songyuanqiao','123456','宋远桥',1,'16.jpg',2,'2007-01-01',2,now(),now()),
(17,'chenyouliang','123456','陈友谅',1,'17.jpg',NULL,'2015-03-21',NULL,now(),now());
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/tlias
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=root
#配置mybatis的日志, 指定输出到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#开启mybatis的驼峰命名自动映射开关 a_column ------> aCloumn
mybatis.configuration.map-underscore-to-camel-case=true
package com.cn.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 部门实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dept {
private Integer id; //ID
private String name; //部门名称
private LocalDateTime createTime; //创建时间
private LocalDateTime updateTime; //修改时间
}
package com.cn.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 员工实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
private Integer id; //ID
private String username; //用户名
private String password; //密码
private String name; //姓名
private Short gender; //性别 , 1 男, 2 女
private String image; //图像url
private Short job; //职位 , 1 班主任 , 2 讲师 , 3 学工主管 , 4 教研主管 , 5 咨询师
private LocalDate entrydate; //入职日期
private Integer deptId; //部门ID
private LocalDateTime createTime; //创建时间
private LocalDateTime updateTime; //修改时间
}
package com.cn.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private Integer code;//响应码,1 代表成功; 0 代表失败
private String msg; //响应信息 描述字符串
private Object data; //返回的数据
//增删改 成功响应
public static Result success(){
return new Result(1,"success",null);
}
//查询 成功响应
public static Result success(Object data){
return new Result(1,"success",data);
}
//失败响应
public static Result error(String msg){
return new Result(0,msg,null);
}
}
/**
*部门
*/
@RestController
@Slf4j
public class DeptController {
@Autowired
private DeptService deptService;
//@RequestMapping(value = "/depts",method = RequestMethod.GET)//指定请求方式为get
@GetMapping("/depts")
public Result list(){
//项目开发中最好不要使用System输出日志,不够专业,可以使用日志框架
//System.out.println("查询全部部门数据");
log.info("查询全部部门数据");
//调用service查询部门数据
List<Dept> list= deptService.list();
return Result.success(list);
}
}
/**
* 部门
*/
public interface DeptService {
//查询全部门数据
List<Dept> list();
}
/**
*部门
*/
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
public List<Dept> list() {
return deptMapper.list();
}
}
/**
*部门
*/
@Mapper
public interface DeptMapper {
//查询全部门数据
@Select("select * from dept")
List<Dept> list();
}
/**
*部门
*/
@RestController
@Slf4j
public class DeptController {
@Autowired
private DeptService deptService;
/**
* 删除部门
* @return
*/
@DeleteMapping("/depts/{id}")
public Result delete(@PathVariable Integer id){
log.info("根据id删除部门:{}",id);
//调用service删除部门
deptService.delete(id);
return Result.success();
}
}
/**
* 删除部门
* @param id
*/
void delete(Integer id);
@Override
public void delete(Integer id) {
deptMapper.deleteById(id);
}
/**
* 根据ID删除部门
* @param id
*/
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);
新增接口文档
新增部门思路分析
/**
*部门
*/
@RestController
@Slf4j
public class DeptController {
@Autowired
private DeptService deptService;
/**
* 新增部门
* @return
*/
@PostMapping("/depts")
public Result add(@RequestBody Dept dept){
log.info("新增部门: {}" , dept);
//调用service新增部门
deptService.add(dept);
return Result.success();
}
}
/**
* 新增部门
* @param dept
*/
void add(Dept dept);
@Override
public void add(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
deptMapper.insert(dept);
}
/**
* 新增部门
* @param dept
*/
@Insert("insert into dept(name, create_time, update_time) values(#{name},#{createTime},#{updateTime})")
void insert(Dept dept);
查看页面原型明确需求:完成分页查询功能(不带查询条件的分页)
需要返回
给前端的数据需要
服务端返回需要
服务端查询返回不需要
服务端返回,前端只需要拿到总记录数可以自己计算出总页数 总页数=总记录数/每页展示的记录数(向上取整)
思路分析
package com.cn.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 分页查询结果封装类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
private Long total; //总记录数,可能比较多选择long类型
private List rows; //数据列表
}
/**
* 员工管理Controller
*/
@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {
@Autowired
private EmpService empService;
/**
* 查看接口文档可知,2个参数要求有默认值
* page:分页查询的页码,如果未指定,默认为1
* pageSize:分页查询的每页记录数,如果未指定,默认为10
* 解决:使用@RequestParam(defaultValue = "xxx")注解,设置默认值
*/
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize){
//{} {} 传递2个参数使用2个占位符,不使用占位符后面参数代表的值无法输出
log.info("分页查询, 参数: {},{}",page,pageSize);
//调用service分页查询
PageBean pageBean = empService.page(page,pageSize);
return Result.success(pageBean);
}
}
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
PageBean page(Integer page, Integer pageSize);
@Autowired
private EmpMapper empMapper;
@Override
public PageBean page(Integer page, Integer pageSize) {
//1. 获取总记录数
Long count = empMapper.count();
//2. 获取分页查询结果列表
Integer start = (page - 1) * pageSize;
List<Emp> empList = empMapper.page(start, pageSize);
//3. 封装PageBean对象
PageBean pageBean = new PageBean(count, empList);
return pageBean;
}
/**
* 查询总记录数
* @return
*/
@Select("select count(*) from emp")
public Long count();
/**
* 分页查询,获取列表数据
* @param start
* @param pageSize
* @return
*/
@Select("select * from emp limit #{start},#{pageSize}")
public List<Emp> page(Integer start, Integer pageSize);
改造代码步骤:
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>1.4.2version>
dependency>
/**
* 员工信息查询
* @return
*/
@Select("select * from emp")
public List<Emp> list();
@Override
public PageBean page(Integer page, Integer pageSize) {
//1. 设置分页参数 :当前页码值 每页显示的记录数
PageHelper.startPage(page,pageSize);
//2. 执行查询
List<Emp> empList = empMapper.list();
//把查询的结果强转为Page类型,之后才可以调用Page的方法
Page<Emp> p = (Page<Emp>) empList;
//3. 封装PageBean对象 p.getTotal():获取总记录数 p.getResult():获取结果列表
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
/**
* 员工管理Controller
*/
@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {
@Autowired
private EmpService empService;
/**
* 查看接口文档可知,2个参数要求有默认值
* page:分页查询的页码,如果未指定,默认为1
* pageSize:分页查询的每页记录数,如果未指定,默认为10
* 解决:使用@RequestParam(defaultValue = "xxx")注解,设置默认值
*/
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
String name, Short gender,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
//{} {} 传递2个参数使用2个占位符,不使用占位符后面参数代表的值无法输出
log.info("分页查询, 参数: {},{},{},{},{},{}",page,pageSize,name,gender,begin,end);
//调用service分页查询
PageBean pageBean = empService.page(page,pageSize,name,gender,begin,end);
return Result.success(pageBean);
}
}
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
PageBean page(Integer page, Integer pageSize,String name, Short gender,LocalDate begin,LocalDate end);
@Autowired
private EmpMapper empMapper;
@Override
public PageBean page(Integer page, Integer pageSize,String name, Short gender,LocalDate begin,LocalDate end) {
//1. 设置分页参数 :当前页码值 每页显示的记录数
PageHelper.startPage(page,pageSize);
//2. 执行查询
List<Emp> empList = empMapper.list(name, gender, begin, end);
//把查询的结果强转为Page类型,之后才可以调用Page的方法
Page<Emp> p = (Page<Emp>) empList;
//3. 封装PageBean对象 p.getTotal():获取总记录数 p.getResult():获取结果列表
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
/**
* 员工信息查询
* @return
*/
//@Select("select * from emp") 使用动态sql编写分页条件查询
public List<Emp> list(String name, Short gender,LocalDate begin,LocalDate end);
mybatis.mapper-locations=classpath:mapper/*.xml
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cn.mapper.EmpMapper">
<select id="list" resultType="com.cn.pojo.Emp">
select * from emp
<where>
<if test="name != null and name != ''">
name like concat('%',#{name},'%')
if>
<if test="gender != null">
and gender = #{gender}
if>
<if test="begin != null and end != null">
and entrydate between #{begin} and #{end}
if>
where>
order by update_time desc
select>
mapper>
@DeleteMapping("/{ids}")
public Result delete(@PathVariable List<Integer> ids){
log.info("批量删除操作, ids:{}",ids);
empService.delete(ids);
return Result.success();
}
/**
* 批量删除
* @param ids
*/
void delete(List<Integer> ids);
@Override
public void delete(List<Integer> ids) {
empMapper.delete(ids);
}
/**
* 批量删除
* @param ids
*/
void delete(List<Integer> ids);
<delete id="delete">
delete
from emp
where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
foreach>
delete>
@PostMapping
public Result save(@RequestBody Emp emp){
log.info("新增员工, emp: {}",emp);
empService.save(emp);
return Result.success();
}
/**
* 新增员工
* @param emp
*/
void save(Emp emp);
@Override
public void save(Emp emp) {
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.insert(emp);
}
/**
* 新增员工
* id:设置的主键自增,不需要插入
* password:设置的有默认约束,不需要插入
* @param emp
*/
@Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) " +
" values(#{username},#{name},#{gender},#{image},#{job},#{entrydate},#{deptId},#{createTime},#{updateTime})")
void insert(Emp emp);
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>3.15.1version>
dependency>
package com.cn.utils;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;
/**
* 阿里云 OSS 工具类
*/
@Component
public class AliOSSUtils {
private String endpoint = "https://oss-cn-beijing.aliyuncs.com";//OSS对象存储服务的地址
private String accessKeyId = "LTAI5tSHskmRrsoA2C1sikMm";//miyao(。。。违反社区规所以这样写)
private String accessKeySecret = "d6WvJcYYhfsdq36Pl4jAt72wkxGO1N";//米妖(。。。)
private String bucketName = "web-wenjian-shangchuan";//工作空间名
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();
// 避免文件覆盖 :uuid生成的前缀+原始文件名的后缀
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径:OSS地址前缀(//前的内容)+存储空间+OSS地址后缀(//后的内容)+文件名
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
package com.cn.controller;
import com.cn.pojo.Result;
import com.cn.utils.AliOSSUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
@Slf4j
@RestController
public class UploadController {
@Autowired
private AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
public Result upload(MultipartFile image) throws IOException {
log.info("文件上传, 文件名: {}", image.getOriginalFilename());
/**
* 调用阿里云OSS工具类,将上传来的文件保存在阿里云中:
* 这个提供的阿里云OSS工具类方法并不是一个静态方法,所以调用这个方法区还需要new一个工具类对象,
* 现在使用的都是Spring环境,所以建议把工具类交给Spring容器管理(在工具类上添加@Component),
* 在注入即可
*/
String url = aliOSSUtils.upload(image);
log.info("文件上传完成,文件访问的url: {}", url);
return Result.success(url);//将图片上传后的url返回,用于浏览器回显展示。
}
}
@GetMapping("/{id}")
public Result getById(@PathVariable Integer id){
log.info("根据ID查询员工信息, id: {}",id);
Emp emp = empService.getById(id);
return Result.success(emp);
}
/**
* 根据ID查询员工
* @param id
* @return
*/
Emp getById(Integer id);
@Override
public Emp getById(Integer id) {
return empMapper.getById(id);
}
/**
* 根据ID查询员工
* @param id
* @return
*/
@Select("select * from emp where id = #{id}")
Emp getById(Integer id);
@PutMapping
public Result update(@RequestBody Emp emp){
log.info("更新员工信息 : {}", emp);
empService.update(emp);
return Result.success();
}
/**
* 更新员工
* @param emp
*/
void update(Emp emp);
@Override
public void update(Emp emp) {
emp.setUpdateTime(LocalDateTime.now());
empMapper.update(emp);
}
/**
* 更新员工
* @param emp
*/
void update(Emp emp);
<update id="update">
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="image != null and image != ''">
image = #{image},
if>
<if test="job != null">
job = #{job},
if>
<if test="entrydate != null">
entrydate = #{entrydate},
if>
<if test="deptId != null">
dept_id = #{deptId},
if>
<if test="updateTime != null">
update_time = #{updateTime}
if>
set>
where id = #{id}
update>
当前项目中代码所存在的一些问题:完成文件上传功能时调用阿里云OSS工具类是把参数直接写死在类中。这样项目中每涉及到一个技术或者第三方的服务就将参数硬编码在java代码中,就会存在2个问题
。
.java--》.class
),重新运行,这样非常的繁琐。解决:把参数定义在配置文件中,之后在代码中引入即可。
#阿里云OSS配置
# properties配置文件中信息本身就是一个字符串所以不需要引号,不需要分号,等号两边的空格也不需要。
aliyun.oss.endpoint=https://oss-cn-beijing.aliyuncs.com
aliyun.oss.accessKeyId=LTAI5tSHskmRrsoA2C1sikMm
aliyun.oss.accessKeySecret=d6WvJcYYhfsdq36Pl4jAt72wkxGO1N
aliyun.oss.bucketName=wenjian
//写的是properties文件中的key,注意是Spring家的注解
@Value("${aliyun.oss.endpoint}")
private String endpoint; //OSS对象存储服务的地址
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId; //秘钥
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret; //秘钥
@Value("${aliyun.oss.bucketName}")
private String bucketName; //工作空间名
spring:
#数据库连接信息
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/tlias
username: root
password: root
#文件上传的配置
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
#Mybatis配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
mapper-locations: classpath:mapper/*.xml
#阿里云OSS
aliyun:
oss:
endpoint: https://oss-cn-beijing.aliyuncs.com
accessKeyId: LTAI5tSHskmRrsoA2C1sikMm
accessKeySecret: d6WvJcYYhfsdq36Pl4jAt72wkxGO1N
bucketName: web-wenjian-shangchuan
package com.cn.utils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
package com.cn.utils;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;
/**
* 阿里云 OSS 工具类
*/
@Component
public class AliOSSUtils {
/* //写的是properties文件中的key,注意是Spring家的注解
@Value("${aliyun.oss.endpoint}")
private String endpoint; //OSS对象存储服务的地址
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId; //秘钥
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret; //秘钥
@Value("${aliyun.oss.bucketName}")
private String bucketName; //工作空间名*/
@Autowired
private AliOSSProperties aliOSSProperties;
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile file) throws IOException {
//获取阿里云OSS参数
String endpoint = aliOSSProperties.getEndpoint();
String accessKeyId = aliOSSProperties.getAccessKeyId();
String accessKeySecret = aliOSSProperties.getAccessKeySecret();
String bucketName = aliOSSProperties.getBucketName();
// 获取上传的文件的输入流
InputStream inputStream = file.getInputStream();
// 避免文件覆盖 :uuid生成的前缀+原始文件名的后缀
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径:OSS地址前缀(//前的内容)+存储空间+OSS地址后缀(//后的内容)+文件名
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
spring-boot-configuration-processor
依赖,此依赖的作用是,自动的识别被@ConfigurationProperties注解标识的Bean对象,之后再配置文件中进行配置的时候,就会自动的提示与这个Bean对象的属性名相对应的配置项名字。即:在配置文件中配置阿里云OSS的配置信息时就会有对应的提示,不影响程序运行是可选的操作。 <dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("员工登录: {}", emp);
Emp e = empService.login(emp);
//登录失败, 返回错误信息
return e !=null? Result.success():Result.error("用户名或密码错误");
}
}
/**
* 员工登录
* @param emp
* @return
*/
Emp login(Emp emp);
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
/**
* 根据用户名和密码查询员工
* @param emp
* @return
*/
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
登录校验
:服务端接收客户端发送过来的请求,首先需要对请求进行校验用户是否登录,如果登录了就执行对应的业务操作,如果没有登录就不允许执行相关的业务操作,而是返回给前端一个错误的结果最终跳转到登录页面。解决
:可以使用统一拦截的技术(拦截器、过滤器)拦截浏览器发送的所有请求,之后获取登录成功后的标记并且这个标记也没有问题,那么说明员工已经登录进行放行,否则响应给前端一个错误信息,前端就会自动的跳转到登录页面。多次
请求和响应。
共享数据
。
自动的
将Cookie响应给浏览器。自动的
存储在浏览器本地,之后的每一次请求都会携带本地存储的Cookie自动的
携带到服务端。package com.cn.controller;
import com.cn.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* Cookie、HttpSession演示
*/
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
//创建Cookie对象,并知客户端保存Cookie
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
//通过request获取浏览器请求头中携带的Cookie
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}
两种方式获取Session
),如果是第一次请求Session会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session,每一个会话对象Session都会有一个Session的Id,之后服务器端在给浏览器响应数据的时候,他会将这个Session的id通过Cookie响应给浏览器,即:在响应头中加了Set-Cookie这个响应头,这个Cookie的名字是固定的JSESSIONID
package com.cn.controller;
import com.cn.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* Cookie、HttpSession演示
*/
@Slf4j
@RestController
public class SessionController {
/**
* 方式一:直接在控制层方法中设置HttpSession类型的参数获取Session
*
* 在控制层方法中声明一个Session,之后服务器端会进行判断当前请求
* 对应的Session是否存在,如果不存在他会创建一个新的Session,如果存在
* 他会获取到当前请求对应的Session
*/
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info("HttpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom"); //往session中存储数据
return Result.success();
}
/**
* 方式二:在方法中声明一个HttpServletRequest对象类型的参数,
* 之后通过request对象的方法来获取当前请求对应的Session
*/
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info("HttpSession-s2: {}", session.hashCode());
Object loginUser = session.getAttribute("loginUser"); //从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
支持PC端、移动端
:因为现在并不需要把令牌必须保存在Cookie中,其他任何的存储空间当中都是可以的,你只需要在客户端当中将这个令牌存储起来就可以了。解决集群环境下的认证问题
:因为我在服务器端并不需要存储任何的数据减轻服务器端存储压力
:正是因为在服务端不需要存储任何的数据,所以他也减轻了服务器端的存储压力。需要自己实现
:怎么样生成令牌,怎么样将令牌存储在客户端浏览器,怎么样将令牌携带到服务端这些都需要我们自己实现。当然在实际的开发中也需要前端的开发人员来配合实现。简介:https://jwt.io/
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
package com.cn;
import com.cn.mapper.EmpMapper;
import com.cn.pojo.Emp;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @SpringBootTest的作用:如果想要使用注入组件功能,需要类上添加@SpringBootTest注解标识,
* 这里显然不需要使用,所以可以把此注解注释掉。好处是在运行的时候不用加载整个Spring的运行环境,
* 可以提高运行速度。
*/
//@SpringBootTest
class SpringBootCrudHeimaApplicationTests {
/**
* 生成JWT
*/
@Test
public void testGenJwt(){
Map<String, Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("name","tom");
/* Jwts.builder():构建JWT令牌,之后通过链式编程来调用里面的方法来设置gwt令牌在生成的时候所需要设置的一些参数:
* signWith():指定签名算法、秘钥(秘钥就是一个字符串,自己设置一个)
* setClaims():JWT令牌所存储的内容,也就是自定义的数据(载荷 第二部分数据)。参数类型为map集合,
* 可以把自定义的数据封装到map集合中
* setExpiration():设置令牌的有效期,在有效时间范围内生效,超出有效期就失效了。
* 参数类型为Date类型:拿到当前时间在加上一个小时,表示当前时间向后推迟一个小时。
* System...获取到的是当前时间的时毫秒值,所以把3600秒乘以一千转化为毫秒值后在相加。
* compact():调用此方法后就可以拿到一个字符串类型的返回值,即生成的jwt令牌。
*/
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "itheima")
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期为1h
.compact();
System.out.println(jwt);
}
/**
* 解析JWT
*/
@Test
public void testParseJwt(){
/* Jwts.parser():进行令牌解析
* setSigningKey():指定之前设置的签名秘钥
* parseClaimsJws():指定需要解析的令牌字符串
* getBody():调用此方法就可以拿到自定义的内容,也就是jwt令牌的第二部分内容。
* */
Claims claims = Jwts.parser()
.setSigningKey("itheima")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY4NTk2MTY5NH0.YDNV-sLr28gKzCSYaknefgftVMAM2VvIIPaW26qGu68")
.getBody();
//解析后的结果是一个Map集合
System.out.println(claims);
}
}
package com.cn.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static String signKey = "itheima"; //指定秘钥
private static Long expire = 43200000L; //指定过期时间为12小时
/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @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;
}
}
package com.cn.controller;
import com.cn.pojo.Emp;
import com.cn.pojo.Result;
import com.cn.service.EmpService;
import com.cn.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("员工登录: {}", emp);
Emp e = empService.login(emp);
//登录成功,生成令牌,下发令牌
if (e != null){ //调用login()查询到了员工的信息说明登录成功了。
Map<String, Object> claims = new HashMap<>();
claims.put("id", e.getId());
claims.put("name", e.getName());
claims.put("username", e.getUsername());
String jwt = JwtUtils.generateJwt(claims); //调用工具类生成jwt令牌,jwt包含了当前登录的员工信息
return Result.success(jwt);//登陆成功后下发令牌。由接口文档可知返回的是json类型的数据
}
//登录失败, 返回错误信息 由接口文档可知返回的是json类型的数据
return Result.error("用户名或密码错误");
}
}
Filter过滤器
,是JavaWeb三大组件(Servlet、Filter、Listener)之一。拦截
下来,从而实现一些特殊的功能。通用
的操作,比如:登录校验、统一编码处理、敏感字符处理等。步骤:
@WebFilter
注解,配置拦截资源的路径。启动类类上加@ServletComponentScan
开启Servlet组件支持。package com.cn.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*") //拦截所有请求
/*注意实现过滤器之后需要重写的3个方法,只有doFilter方法是必须要重写
的,而其他的2个方法在父接口中已经做了默认实现所以如果不用的话就不
需要重写了(因为这2个方法不经常用,所以底层使用了default标识,jdk8)*/
public class DemoFilter implements Filter {
/*
* 初始化方法, 只调用一次
* web服务器启动时自动创建Filter对象并调用初始化方法,通常
* 完成一些资源以及环境的准备操作。
* */
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init 初始化方法执行了");
}
//每次拦截到请求之后都会调用, 调用多次
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Demo 拦截到了请求...放行前逻辑");
//放行
chain.doFilter(request,response);
System.out.println("Demo 拦截到了请求...放行后逻辑");
}
//销毁方法, 只调用一次 关闭服务器时候调用,通常做些环境的清理以及资源的释放操作
@Override
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
package com.cn.filter;
import com.alibaba.fastjson.JSONObject;
import com.cn.pojo.Result;
import com.cn.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//下面用的方法都是HttpServletRequest对象中的方法,而这里重写后的方法是ServletRequest类型,所以需要强转
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//1.获取请求url。
String url = req.getRequestURL().toString();
log.info("请求的url: {}",url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){
log.info("登录操作, 放行...");
chain.doFilter(request,response);
//判断是登录操作后放行,放行后不需要在接着执行以下业务逻辑代码,所以使用return结束方法执行。
return;
}
//3.不是登录操作,获取请求头中的令牌(token)。
String jwt = req.getHeader("token");
/* 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
StringUtils.hasLength(): Spring提供的工具类 如果字符序列不为 null 值,
并且字符序列的长度大于 0 ,则返回 true
情况1:令牌存在,有值,返回true,加上!变为false,所以此if判断不会执行。
情况2:令牌不存在,美有值,返回false,加上!变为true,所以此if判断会执行。
*/
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
/*由接口文档可知未登录需要返回给前端一个错误的信息,并且是json类型,
* 之前是在控制层方法中加上@ResponseBody注解,就会自动将方法的返回值转化为json并返回给前端,
* 现在在过滤器中并不是在Controller中,所以需要手动的进行转换。
* 转换步骤:引入阿里巴巴提供的json转换依赖fastJSON
* 调用里面的工具Api方法进行转换
* */
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
//把转化后的错误信息json对象返回给前端,注意使用的是原生的方式,因为这是过滤器中不是在控制层方法
resp.getWriter().write(notLogin);
//结束方法,不需要执行下面的代码
return;
}
//5.如果令牌存在,解析token,如果解析失败,返回错误结果(未登录)。
/* 利用jwt令牌解析失败会报异常这一特点,可以使用try-catch来区分什么时候
解析成功,什么时候解析失败。
如果出现异常说明,jwt解析失败,没有出现异常说明jwt解析成功。
*/
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//异常捕获到了,说明出现了异常,jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
//通用解析失败需要返回一个json类型的错误信息,由接口文档可知
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//6.上面没有捕获到异常,令牌存在且解析成功,说明登陆过了,放行。
log.info("令牌合法, 放行");
chain.doFilter(request, response);
}
}
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.76version>
dependency>
使用Postman进行debug测试:
前后端联调测试:
步骤:
package com.cn.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行, 返回true: 放行, 放回false, 不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
System.out.println("preHandle ...");
return true;
}
@Override //目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ...");
}
@Override //视图渲染完毕后运行, 最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
package com.cn.config;
import com.cn.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration //标识当前类是一个配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*注册拦截器,参数为之前创建的拦截器对象。因为拦截器交给了spring管理所以直接注入即可.
如果没有在拦截器的类上添加@Component注解,此时使用new的方式即可。
*/
registry.addInterceptor(loginCheckInterceptor)
//指定需要拦截的资源,拦截所有在过滤器器是/*,在拦截器中是/**。
.addPathPatterns("/**");
}
}
package com.cn.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.cn.pojo.Result;
import com.cn.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//登录校验的逻辑定义在preHandle方法中,因为只有这个方法才是在目标方法之前执行,
// controller目标方法执行过了就没必要做校验了
@Override //目标资源方法运行前运行, 返回true: 放行, 放回false, 不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//1.获取请求url。这个地方传递过来的就是HttpServletRequest对象,不需要在强转了
String url = req.getRequestURL().toString();
log.info("请求的url: {}",url);
//这一步操作因为在拦截器配置类中,设置了不拦截登录请求,所以加不加都可以。
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){
log.info("登录操作, 放行...");
return true;
}
//3.不是登录操作,获取请求头中的令牌(token)。
String jwt = req.getHeader("token");
/* 4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
StringUtils.hasLength(): Spring提供的工具类 如果字符序列不为 null 值,
并且字符序列的长度大于 0 ,则返回 true
情况1:令牌存在,有值,返回true,加上!变为false,所以此if判断不会执行。
情况2:令牌不存在,没有值,返回false,加上!变为true,所以此if判断会执行。
*/
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
//令牌不存在说明没有登录不能放行,放行的话会去执行Controller接口的目标方法了
return false;
}
//5.解析token,如果解析失败,返回错误结果(未登录)。
/* 利用jwt令牌解析失败会报异常这一特点,可以使用try-catch来区分什么时候
解析成功,什么时候解析失败。
如果出现异常说明,jwt解析失败,没有出现异常说明jwt解析成功。
*/
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//异常捕获到了,说明出现了异常,jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
//同样令牌解析失败说明没有登录不能放行,放行的话会去执行Controller接口的目标方法了
return false;
}
//6.上面没有捕获到异常,令牌存在且解析成功,说明登陆过了,放行。
log.info("令牌合法, 放行");
return true;
}
@Override //目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ...");
}
@Override //视图渲染完毕后运行, 最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
package com.cn.config;
import com.cn.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration //标识当前类是一个配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*注册拦截器,参数为之前创建的拦截器对象。因为拦截器交给了spring管理所以直接注入即可.
如果没有在拦截器的类上添加@Component注解,此时使用new的方式即可。
*/
registry.addInterceptor(loginCheckInterceptor)
//指定需要拦截的资源,拦截所有在过滤器器是/*,在拦截器中是/**。
.addPathPatterns("/**")
//配置不需要拦截的路径
.excludePathPatterns("/login");
}
}
思考:程序开发过程中不可避免的会遇到异常现象,此时在当前项目中我们并未做异常处理,那么一旦出现异常项目的流程是如何执行的呢???
方案一(不推荐):由于最终异常会抛给控制层的代码,所以只需要在每一个控制层方法中做异常处理(try-catch)就行了。
方案二(推荐):全局异常处理器,只要在项目中定义全局异常处理器就可以处理项目中所有的异常
。
package com.cn.exception;
import com.cn.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器:
* 定义一个类添加@RestControllerAdvice注解,此时就代表我们定义了一个全局异常处理器
* @RestControllerAdvice = @ControllerAdvice + @ResponseBody 所以它会将方法的返回值转化为json在
* 返回,符合接口文档中规定的需要返回json类型的数据格式。
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/*在全局异常处理器中定义一个方法来捕获异常,在此方法上通过@ExceptionHandler注解
的value属性来设置需要捕获那些异常*/
@ExceptionHandler(Exception.class)//捕获所有异常
public Result ex(Exception ex){
/*捕获到异常后,就可以在这个方法中处理异常,这里只是简单地输出了下异常
的堆栈信息,然后响应一个标准的Result,在Result中封装错误的提示信息,
最终将result响应给前端*/
ex.printStackTrace();//输出异常的堆栈信息
return Result.error("对不起,操作失败,请联系管理员");
}
}
案例需求:删除部门,同时删除该部门下的员工。
说明:部门解散了,我们不仅要把部门删除了,还需要把该部门下对应的员工数据删除。
当前项目中的删除部门功能,只是完成了根据id删除部门数据,并没有删除该部门下的员工数据,这个逻辑是不完整的。如果仅仅是把部门删除了那么部门下的员工还会关联着这个部门,此时就会造成数据的不完整不一致。
改造代码:添加删除部门,同时删除该部门下的员工数据的代码。
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Override
public void delete(Integer id) {
//根据id删除部门数据
deptMapper.deleteById(id);
//根据部门ID删除该部门下的员工
empMapper.deleteByDeptId(id);
}
/**
* 根据部门ID删除该部门下的员工数据
* @param deptId
*/
@Delete("delete from emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);
演示异常执行流程:
为什么会出现这种情况呢???
添加到业务层中来控制事物
,因为在业务层中,一个业务功能中可能会包含多个数据访问的操作,这样就可以将多个数据访问操作控制在一个事务范围内。作用在方法上
表示当前方法交给Spring进行事务管理,作用在类上
表示当前类中的所有方法交给Spring进行事务管理,作用在接口上
表示当前接口的所有实现类中的所有实现方法交给Spring进行事务管理。加在业务层执行多次数据访问操作这一类的增删改方法上
。
查询操作
并不会影响数据的变更所以无需控制事务
,如果在业务层只是执行了一步的增删改操作
我们也不需要控制事务
,因为Mysql的数据库事务是自动提交的,我们DML(数据操作语言-crud)语句执行完后事务已经自动提交了,如果执行失败数据库中的数据也不会发生变化。运行测试:删除学工部,发现删除操作出现异常事务会进行回滚,数据库表中的数据并未被删除掉。
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
说明:解散部门是一个非常重要非常危险的操作,所以在业务当中要求每一次执行解散部门的操作时候,都需要留下痕迹 即:无论执行成功了还是失败了都需要记录日志
。
第一步的操作已经完成过了,所以我们只需要完成第二步操作即可。
CREATE TABLE dept_log(
id INT AUTO_INCREMENT COMMENT '主键ID' PRIMARY KEY,
create_time DATETIME NULL COMMENT '操作时间',
description VARCHAR(300) NULL COMMENT '操作描述'
)COMMENT '部门操作日志表';
package com.cn.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
private Integer id;
private LocalDateTime createTime;
private String description;
}
package com.cn.service.impl;
import com.cn.mapper.DeptMapper;
import com.cn.mapper.EmpMapper;
import com.cn.pojo.Dept;
import com.cn.pojo.DeptLog;
import com.cn.service.DeptLogService;
import com.cn.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
*部门
*/
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;
@Transactional(rollbackFor = Exception.class) //spring事务管理
@Override
public void delete(Integer id) {
/*因为要求日志操作无论程序是否执行成功,都需要记录日志,所以放到finally当中,
又因为异常已经统一处理过了,所以不需要catch代码部分 */
try {
//根据id删除部门数据
deptMapper.deleteById(id);
//模拟异常
int i = 1/0;
//根据部门ID删除该部门下的员工
empMapper.deleteByDeptId(id);
} finally {
//记录删除操作的日志
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次解散的是"+id+"号部门");
deptLogService.insert(deptLog);
}
}
}
package com.cn.service;
import com.cn.pojo.DeptLog;
public interface DeptLogService {
void insert(DeptLog deptLog);
}
package com.cn.service.impl;
import com.cn.mapper.DeptLogMapper;
import com.cn.pojo.DeptLog;
import com.cn.service.DeptLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
package com.cn.mapper;
import com.cn.pojo.DeptLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeptLogMapper {
@Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
void insert(DeptLog log);
}
启动项目执行部门删除操作:发现出现异常后没有执行日志记录操作,之前要求是无论部门删除成功还是失败都会被记录日志,为此记录日志的代码放到了finally中,那么为什么日志没有记录成功呢????
原因:
@Transactional(propagation = Propagation.REQUIRED)
,表示部门删除的方法在调用日志记录的方法时候,如果A方法有事务那么B方法加入到A的事务中,如果A方法没有事务那么B方法开启一个新的事物,所以这里显然是日志记录的方法加入到了删除部门的事务方法中。解决:
@Transactional(propagation = Propagation.REQUIRES_NEW)
:需要新事物,在调用b方法的时候无论a方法是否有事务,b方法都会在一个新的事务中运行。AOP
:Aspect Oriented Programming (面向切面编程、面向方面编程
),其实就是面向特定方法编程。
场景: 在一个项目中开发了很多功能,但是有一些功能的运行速度比较慢,所以现在需要优化这个项目。那么第一步就需要先定位出执行比较耗时的业务方法,然后在针对这些业务方法进行优化。
传统方式
:统计项目中的每一个业务方法的耗时,可以在每一个业务方法运行之前记录这个方法的开始时间,在这个方法运行完毕后再来记录这个方法的结束时间,然后那这个方法的结束时间减去方法的开始时间就是这个方法的执行耗时。缺点
:一个项目中包含很多的业务模块,每一个业务模块都会包含很多增删改查的方法,这样修改每一个业务方法的代码来记录时间,会变得非常繁琐。使用AOP的方式
:可以做到再不改变原始方法的基础上来针对原始方法进行编程,这个编程可以是对原始方法的增强也可以是改变原始方法的功能。此时想要统计各个方法的耗时的需求只需要定义一个模版方法,然后将记录方法执行耗时的这一部分公共的逻辑代码定义在这个模版方法当中,在这个方法开始运行之前记录方法开始运行的时间,在方法执行之后记录方法的结束时间,中间来运行原始的业务方法。使用AOP的执行流程
:假如现在项目要进行查询所有的部门,此时就会调用部门管理中的list方法,但是它并不会直接执行这个原始的list方法,而是会自动执行这个模版方法,模版方法在运行的时候就会先去统计这个方法运行的开始时间,然后在来执行原始的业务方法list方法,原始方法执行完毕后在来执行方法的结束时间,结束时间减去开始时间就是原始方法的执行耗时。实现:
步骤:
案例测试:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component //把当前类交给Spring容器去管理
@Aspect //标识当前类是一个AOP类
public class TimeAspect {
//切入点表达式:指定当前这一部分共性逻辑的代码会作用在哪些方法上
//表示service下的所有接口或者类里面的所有方法,都会运行这个公共方法所封装的公共逻辑代码
@Around("execution(* com.itheima.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
//1. 记录开始时间
long begin = System.currentTimeMillis();
/* 2. 调用原始方法运行
* 需要借助AOP给我们提供的一个Api,在方法的行参中声明一个ProceedingJoinPoint类型
* 的参数,之后调用proceed方法就代表运行原始方法。
* 原始方法运行过程中可能会出现异常,所以我们需要处理,这里使用throws抛出。
* 原始方法在运行的时候会有返回值,所以需要使用Object类型来接收。
* 现在方法执行完毕后只是统计方法的执行耗时,所以需要把原始方法的返回值返回回去。
* */
Object result = joinPoint.proceed();
//3. 记录结束时间, 计算方法执行耗时
long end = System.currentTimeMillis();
/*joinPoint这个对象会封装原始方法的相关信息,所以可以调用getSignature()
方法来获取原始方法的签名,这样就可以知道是那个方法执行的耗时时间。
*/
log.info(joinPoint.getSignature()+"方法执行耗时: {}ms", end-begin);
return result;
}
}
之后在程序注入的时候就不再是注入目标对象了而是注入了代理对象
,在调用list方法之后其实就是调用的代理对象中的list方法,而这个方法已经进行了功能的增强、没有异常执行:
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect1 {
@Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void before(){
log.info("before ...");
}
@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
log.info("around after ...");
return result;
}
@After("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void after(){
log.info("after ...");
}
@AfterReturning("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void afterReturning(){
log.info("afterReturning ...");
}
@AfterThrowing("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void afterThrowing(){
log.info("afterThrowing ...");
}
}
有异常执行:
运行测试
:
测试:
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class MyAspect1 {
/**
* 相同切入点抽取:1.定义一个无返回值类型、无参、无方法体的方法,名字随意,
* 使用@Pointcut注解描述,值为切入点表达式
* 2.之后在具体使用的切入点表达式中引用方法名即可
*/
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pt(){}
@Before("pt()")
public void before(){
log.info("before ...");
}
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
log.info("around after ...");
return result;
}
@After("pt()")
public void after(){
log.info("after ...");
}
@AfterReturning("pt()")
public void afterReturning(){
log.info("afterReturning ...");
}
@AfterThrowing("pt()")
public void afterThrowing(){
log.info("afterThrowing ...");
}
}
说明:
测试:
测试:
测试:
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
//切面类
@Slf4j
@Aspect
@Component
public class MyAspect6 {
//完整写法:
//@Pointcut("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//省略权限修饰符:
//@Pointcut("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
//省略权限修饰符、包名类名: 但是包名.类名不建议省略,因为匹配的范围过大影响效率
//@Pointcut("execution(void delete(java.lang.Integer))")
//基于接口匹配,上面的都是基于实现类匹配:
//@Pointcut("execution(void com.itheima.service.DeptService.delete(java.lang.Integer))")
//使用通配符:*
//@Pointcut("execution(void com.itheima.service.DeptService.*(java.lang.Integer))")
//@Pointcut("execution(* com.*.service.DeptService.*(*))")
//@Pointcut("execution(* com.itheima.service.*Service.delete*(*))")
//使用通配符:..
//@Pointcut("execution(* com.itheima.service.DeptService.*(..))")
//@Pointcut("execution(* com..DeptService.*(..))") com开头任意层级下的包
//@Pointcut("execution(* com..*.*(..))") //com开头任意层级下的包中,所有类中的所有方法
//@Pointcut("execution(* *(..))") //慎用 当前环境下的所有方法,方法的形参也是任意的
// 使用一个切入点表达式匹配2个方法:这2个方法的返回值不同,方法名也没有相同的前缀后缀,一个是有参的一个是无参的
// 解决:一个一个的匹配,中间使用||连接,表示满足其中一个表达式都会运行通知
@Pointcut("execution(* com.itheima.service.DeptService.list()) || " +
"execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
private void pt(){}
@Before("pt()")
public void before(){
log.info("MyAspect6 ... before ...");
}
}
package com.itheima.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) //注解什么时候生效
@Target(ElementType.METHOD) //注解作用在哪些位置上
public @interface MyLog {
}
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
//切面类
@Slf4j
@Aspect
@Component
public class MyAspect7 {
//匹配DeptServiceImpl中的 list() 和 delete(Integer id)方法
//@Pointcut("execution(* com.itheima.service.DeptService.list()) || execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
@Pointcut("@annotation(com.itheima.aop.MyLog)") //指定注解的全类名,表示匹配方法上加的有MyLog这个注解的方法
private void pt(){}
@Before("pt()")
public void before(){
log.info("MyAspect7 ... before ...");
}
}
JoinPoint
抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
测试:
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.Arrays;
//切面类
@Slf4j
@Aspect
@Component
public class MyAspect8 {
@Pointcut("execution(* com.itheima.service.DeptService.*(..))")
private void pt(){}
@Before("pt()")
public void before(JoinPoint joinPoint){ //注意是lang包下
log.info("MyAspect8 ... before ...");
}
//JoinPoint和ProceedingJoinPoint的APi都是一样的,所以以环绕通知为例
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("MyAspect8 around before ...");
//1. 获取 目标对象的类名 .
String className = joinPoint.getTarget().getClass().getName();
log.info("目标对象的类名:{}", className);
//2. 获取 目标方法的方法名 .
String methodName = joinPoint.getSignature().getName();
log.info("目标方法的方法名: {}",methodName);
//3. 获取 目标方法运行时传入的参数 .
Object[] args = joinPoint.getArgs();
log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));
//4. 放行 目标方法执行 .
Object result = joinPoint.proceed();
//5. 获取 目标方法运行的返回值 .
log.info("目标方法运行的返回值: {}",result);
log.info("MyAspect8 around after ...");
return result;
}
}
说明:
准备:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_user int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(1000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
package com.cn.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer id; //ID
private Integer operateUser; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
package com.cn.mapper;
import com.cn.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OperateLogMapper {
//插入日志数据
@Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
"values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
public void insert(OperateLog log);
}
编码:
package com.cn.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
}
package com.cn.aop;
import com.alibaba.fastjson.JSONObject;
import com.cn.mapper.OperateLogMapper;
import com.cn.pojo.OperateLog;
import com.cn.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;
@Slf4j
@Component
@Aspect //切面类
public class LogAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.cn.anno.Log)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
/* 1.操作人ID - 当前登录员工ID:
* 思路分析:登陆成功后服务端会生成一个jwt令牌给前端,之后前端每次发送请求
* 都会通过请求头token携带令牌到服务端,而令牌当中包含了员工的信息,
* 所以我们可以通过请求头来获取令牌,然后在令牌中获取员工的id即可
*
* 请求头需要通过request对象获取:
* 之前:在控制层方法中可以直接声明一个HttpServletRequest类型的参数即可
* 现在:在切面类中获取request对象,而切面类当中通知方法的参数又不能随便定义,
* 可以直接 @Autowired注入这个HttpServletRequest对象。
* */
String jwt = request.getHeader("token");//获取请求头中的令牌
Claims claims = JwtUtils.parseJWT(jwt); //解析令牌,值为Map集合
Integer operateUser = (Integer) claims.get("id");//根据k获取v,结果为Object类型,所以可以强转为Integer
//2.操作时间
LocalDateTime operateTime = LocalDateTime.now();//当前时间
//3.操作类名
String className = joinPoint.getTarget().getClass().getName();
//4.操作方法名
String methodName = joinPoint.getSignature().getName();
//5.操作方法参数
Object[] args = joinPoint.getArgs();//获取的结果是数组
String methodParams = Arrays.toString(args);//pojo对象中使用的是String类型的变量接收,所以需要进行转化
long begin = System.currentTimeMillis();
//调用原始目标方法运行
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
//6.方法返回值:pojo对象中使用的是String类型接收的返回值,所以需要把获取的对象
// 转化为json格式的字符串存储起来。---通过阿里巴巴提供的工具包fastJSON
String returnValue = JSONObject.toJSONString(result);
//7.操作耗时
Long costTime = end - begin;
//记录操作日志: 通过有参构造给对象属性赋值,之后把数据插入到日志表当中。id设置的有主键自增所 以不需要获取
OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
operateLogMapper.insert(operateLog);
log.info("AOP记录操作日志: {}" , operateLog);
return result;
}
}