新建项目、更改application.properties
配置:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/employee_manager
spring.datasource.username=root
spring.datasource.password=
# the log of mybatis
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
项目结构:
1 部门管理
1.1 部门列表查询
GET
路径:/depts
响应数据:
{
"code": 1,
"msg": "success",
"data":[
{"id":1,"name":"学工部","createTime":"2023-06-16T18:12:36","updateTime":"2023-06-16T18:12:36"},
{"id":2,"name":"教研部","createTime":"2023-06-16T18:12:36","updateTime":"2023-06-16T18:12:36"},
{"id":3,"name":"咨询部","createTime":"2023-06-16T18:12:36","updateTime":"2023-06-16T18:12:36"},
{"id":4,"name":"就业部","createTime":"2023-06-16T18:12:36","updateTime":"2023-06-16T18:12:36"},
{"id":5,"name":"人事部","createTime":"2023-06-16T18:12:36","updateTime":"2023-06-16T18:12:36"}]
}
依次编写Controller
、Service
和Mapper
。
启动前端程序,双击nginx.exe
即可。
端口为90:
1.2 删除部门
DELETE
路径:/depts/{id}
根据ID删除部门数据
1.3 新增部门
POST
路径:/depts
参数:json形式
{
"name": "教研部"
}
添加部门数据
数据库表中已经设置了主键 i d id id是自增的。
1.4 根据部门id查询
GET
路径:/depts/{id}
返回结果:
{
"code": 1,
"msg": "success",
"data": [
{
"id": 1,
"name": "计算机学院",
"createTime": "2022-09-01T23:06:29",
"updateTime": "2022-01-01T23:06:29"
}
]
}
1.5 修改部门名称
请求路径:/depts
请求方式:PUT
参数:
{
"id": 1,
"name": "教研部"
}
更改编号id的部门名称为name
2 员工管理
2.1 员工列表分页查询 PageHelper⭐
请求路径:/emps
请求方式:GET
参数:
【必须】page=1&pageSize=10
【可选】name=张&gender=1&begin=2007-09-01&end=2022-09-01
接口描述:该接口用于员工列表数据的条件分页查询,并返回总记录数
【有一说一我觉得这个接口设计得很烂,应该把两个功能拆开的,明明是独立的功能,但为了教学也可以理解】
请求示例
/emps?name=张&gender=1&begin=2007-09-01&end=2022-09-01&page=1&pageSize=10
返回示例:
{
"code": 1,
"msg": "success",
"data": {
"total": 2,
"rows": [
{
"id": 1,
"username": "jinyong",
"password": "123456",
"name": "金庸",
"gender": 1,
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
"job": 2,
"entrydate": "2015-01-01",
"deptId": 2,
"createTime": "2022-09-01T23:06:30",
"updateTime": "2022-09-02T00:29:04"
},
{
"id": 2,
"username": "zhangwuji",
"password": "123456",
"name": "张无忌",
"gender": 1,
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
"job": 2,
"entrydate": "2015-01-01",
"deptId": 2,
"createTime": "2022-09-01T23:06:30",
"updateTime": "2022-09-02T00:29:04"
}
]
}
}
分析:
# 从0开始查找5条数据
select * from emp limit 0, 5
# 查找第page页的数据
select * from emp limit (page - 1) * page, pageSize
获取总记录数:
select count(*) from emp
调用Mapper
的两个函数来实现功能。
【分页查询插件 PageHelper】
maven依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
更改后:
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
EmpMapper empMapper;
@Override
public PageBean pageList(Integer page, Integer pageSize) {
// 1、设置分页参数
PageHelper.startPage(page, pageSize);
// 2、执行查询,封装为PageHelper中的page
List<Emp> list = empMapper.selectAll();
Page<Emp> pageHelper = (Page<Emp>) list;
// 3、封装PageBean对象
return new PageBean(pageHelper.getTotal(), pageHelper.getResult());
}
}
package com.koukou.manager.mapper;
import com.koukou.manager.model.Emp;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface EmpMapper {
/**
* 查询Emp表的所有记录
* @return
*/
@Select("select * from emp")
public List<Emp> selectAll();
}
2.2 在分页基础上加入查找条件
注意这里的类型,不是LocalDateTime
:
@Slf4j
@RestController
public class EmpController {
@Autowired
private EmpService empService;
/**
* @param page 页面
* @param pageSize 页面大小
* @param name 搜索关键字
* @param gender 性别
* @param begin 最早入职时间
* @param end 最晚入职时间
* @return Page{记录数, 本页员工列表}
*/
@GetMapping("/emps")
public Result pageList(@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) {
log.info("按要求查询某页员工");
PageBean data = empService.pageList(page, pageSize, name, gender, begin, end);
return Result.success(data);
}
}
Service
先进行分页设置,然后按常规进行条件查询,封装为Page<Emp>
,返回。
@Override
public PageBean pageList(Integer page, Integer pageSize,
String name, Short gender,
LocalDate begin, LocalDate end) {
// 1、设置分页参数
PageHelper.startPage(page, pageSize);
// 2、执行查询,封装为PageHelper中的page
List<Emp> list = empMapper.select(name, gender, begin, end);
Page<Emp> pageHelper = (Page<Emp>) list;
// 3、封装PageBean对象
return new PageBean(pageHelper.getTotal(), pageHelper.getResult());
}
Mapper,xml文件中<需要进行转义,SQL语句末尾没有分号:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.koukou.manager.mapper.EmpMapper">
<select id="select" resultType="com.koukou.manager.model.Emp">
select * from emp
<where>
<if test="name != null">name like concat('%', #{name}, '%')</if>
<if test="gender != null">and gender = #{gender}</if>
<if test="begin != null">and entrydate >= #{begin}</if>
<if test="end != null">and entrydate <= #{end}</if>
</where>
order by update_time desc
</select>
</mapper>
2.3 批量删除员工
DELETE
路径:/emps/{ids},参数为数组
根据ID批量删除员工
例子:/emps/1,2,3
接收数组参数:
@DeleteMapping("/emps/{ids}")
public Result delete(@PathVariable List<Integer> ids) {
empService.delete(ids);
return Result.success();
}
xml文件中删除,注意是#{id}
:
<delete id="delete">
delete from emp where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
2.4 新增员工
请求路径:/emps
请求方式:POST
接口描述:该接口用于添加员工的信息
名称 | 类型 | 是否必须 | 备注 |
---|---|---|---|
username | string | 必须 | 用户名 |
name | string | 必须 | 姓名 |
gender | number | 必须 | 性别, 说明: 1 男, 2 女 |
image | string | 非必须 | 图像 |
deptId | number | 非必须 | 部门id |
entrydate | string | 非必须 | 入职日期 |
job | number | 非必须 | 职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师 |
参数:
{
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
"username": "linpingzhi",
"name": "林平之",
"gender": 1,
"job": 1,
"entrydate": "2022-09-18",
"deptId": 1
}
json格式的数据,用一个实体Bean进行接收。
@Override
public void add(Emp emp) {
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
empMapper.add(emp);
}
<insert id="add">
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})
</insert>
2.5 文件上传
请求路径:/upload
请求方式:POST
接口描述:上传图片接口
参数:image{type = file}
返回:
{
"code": 1,
"msg": "success",
"data": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-0400.jpg"
}
上传文件:
MultipartFile
用于接收上传文件UUID
构造唯一文件名image.transferTo
用于保存文件
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
// 使用MultipartFile接收文件,保存在本地
// 1、构造唯一文件名
// 得到文件类型
String originalName = image.getOriginalFilename();
int index = originalName.lastIndexOf('.');
String type = originalName.substring(index);
// uuid 通用唯一识别码,生成文件名字
String newName = UUID.randomUUID().toString() + type;
// 2、保存文件
image.transferTo(
new File("E:\\leetcode\\project\\manager\\src\\main\\resources\\static\\image\\" + newName)
);
return Result.success();
}
}
# the file upload setting
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
2.6 修改员工信息
名称 | 类型 | 是否必须 | 备注 |
---|---|---|---|
id | number | 必须 | id |
username | string | 必须 | 用户名 |
name | string | 必须 | 姓名 |
gender | number | 必须 | 性别, 说明: 1 男, 2 女 |
image | string | 非必须 | 图像 |
deptId | number | 非必须 | 部门id |
entrydate | string | 非必须 | 入职日期 |
job | number | 非必须 | 职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师 |
> 请求路径:/emps
> 请求方式:PUT
> 接口描述:该接口用于修改员工的数据信息
{
"id": 1,
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
"username": "linpingzhi",
"name": "林平之",
"gender": 1,
"job": 1,
"entrydate": "2022-09-18",
"deptId": 1
}
<update id="edit">
update emp
<set>
<if test="username != null"> username = #{username}, </if>
<if test="name != null"> name=#{name}, </if>
<if test="gender != null"> gender=#{gender}, </if>
<if test="image != null"> image = #{image}, </if>
<if test="deptId != null"> image = #{deptId}, </if>
<if test="entrydate != null"> image = #{entrydate}, </if>
<if test="job != null"> image = #{job}</if>
</set>
where id = #{id}
</update>
3 登录
3.1 密码是否正确
最基本的操作:
@Select("select * from emp where username = #{username} and password = #{password};")
public Emp login(String username, String password);
看看这条查询语句是否返回null。
3.2 登录校验
3.2.1 JWT令牌
JSON Web Token
- json数据格式传输信息:将json数据格式进行安全的封装
依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
生成JWT令牌:
@Test
public void genjwt() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("psw", 188);
// 载荷、签名算法、有效期
String jwt = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, "koukou")
.setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))
.compact();
System.out.println(jwt);
}
eyJhbGciOiJIUzI1NiJ9.eyJwc3ciOjE4OCwiaWQiOjEsImV4cCI6MTY4NzAzNTY2Nn0.K70wIsLMcsO2kVqTRkOjtj6nAijEqGyVRKPZAEhSP44
解析JWT令牌:
@Test
public void parseJwt() {
Claims claims = Jwts.parser()
.setSigningKey("koukou")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJwc3ciOjE4OCwiaWQiOjEsImV4cCI6MTY4NzAzNTY2Nn0.K70wIsLMcsO2kVqTRkOjtj6nAijEqGyVRKPZAEhSP44")
.getBody();
System.out.println(claims);
}
{psw=188, id=1, exp=1687035666}
3.2.2 发令牌
令牌生成:登陆成功后,生成 JWT令牌,并返回给前端
令牌校验:请求到达服务器端后,对令牌进行统一拦截、校验。
工具类:
public class JwtUtils {
private static String signKey = "koukou";
private static Long expire = 12 * 3600 * 1000L;
/**
* 生成Jwt令牌
* @param claims json参数
*/
public static String genJwt(Map<String, Object> claims) {
// 载荷、签名算法、有效期
String jwt = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* 解析JWT令牌
*/
public static Claims parseJwt(String jwt) {
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
3.3 校验令牌 Filter
把对资源的请求拦截下来,从而实现特殊功能。
启动类上加上注解@ServletComponentScan
:
@ServletComponentScan
@SpringBootApplication
public class ManagerApplication {
创建Filter类,重写3个方法:
@Slf4j
@WebFilter("/*")
public class LoginCheckFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化方法,Web服务器启动,创建Filter时调用
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// 拦截请求时调用
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 1、执行放行前逻辑
// (1)获取请求url,判断请求是否为login
String url = req.getRequestURL().toString();
// (2) 如果不是login请求,需要校验令牌
if (!url.contains("login")) {
String jwt = resp.getHeader("token");
// [1]如果jwt不存在
if (jwt == null || jwt.length() <= 0) {
log.info("请求头token为空,未登录");
Result error = Result.error("NOT_LOGIN");
// 手动转换,返回json格式的error
String notLogin = JSONObject.toJSONString(error);
// 响应结果
resp.getWriter().write(notLogin);
return;
}
// [2]解析token
try {
JwtUtils.parseJwt(jwt);
}
catch (Exception e) {
log.info("解析令牌失败,返回未登录错误信息");
String error = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
resp.getWriter().write(error);
return;
}
}
// 2、放行
filterChain.doFilter(request, response);
// 执行放行后逻辑
}
@Override
public void destroy() {
// 销毁方法,服务器关闭时调用
Filter.super.destroy();
}
}
3.4 拦截器Interceptor
写一个拦截器类,继承自HandlerInterceptor
,具有注解@Component
:
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 目标资源方法运行前运行,true放行,false不放行
String url = request.getRequestURL().toString();
if (url.contains("login")) {
log.info("直接放行");
return true;
}
// 得到token
String token = request.getHeader("token");
try {
JwtUtils.parseJwt(token);
}
catch (Exception e) {
// 解析失败,返回错误信息
String errorInfo = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
response.getWriter().write(errorInfo);
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
配置拦截器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 配置类,注册配置拦截器
*/
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
3.5 全局异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result catchExcp(Exception ex) {
ex.printStackTrace();
return Result.error("对不起,操作失败");
}
}
4 事务管理
例子:
删除部门,同时删除部门的所有员工。
1、根据ID删除部门
2、根据部门ID删除所有员工
加入@Transactional
注解,整个方法若执行都执行,若不执行都不执行:
@Transactional
@Override
public void deleteDeptById(Integer id) {
deptMapper.deleteDeptById(id);
empMapper.deleteByDeptId(id);
}
开启事务管理日志:
# the transaction log
logging.level.org.springframework.jdbc.support.JdbcTransactionManager=debug
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRE_NEW)
4.1 AOP
面向特定方法编程。
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Slf4j
@Component
@Aspect
public class TimeAspect {
@Around("execution(* com.koukou.manager.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long begin = System.currentTimeMillis();
Object object = proceedingJoinPoint.proceed();
long end = System.currentTimeMillis();
log.info("执行耗时:{}", end - begin);
return object;
}
}
JoinPoint
:连接点,可以被AOP控制的方法
Advice
:通知,哪些是重复的逻辑
- Around、Before、After、AfterReturning、AfterThrowing
PointCut
:匹配连接点的条件,通知只会在切入点方法执行时被应用
- 切入点表达式
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
// 使用标识注解
@Pointcut("@annotation(com.koukou.aop.myLog)")
Aspect
:切面
执行流程:
顺序: