苍穹外卖笔记
基础笔记
- 接口文档可以导入到 apifox 方便查看
@RequestBody是接受前端的 json 数据,然后反序列化为 Java 对象Result<T>统一封装了返回给前端的数据,code,message,data,之后所有的结果只需要返回一个 Result 对象就行,成功返回 Result.ok(data),错误就返回 Result.error(message)
DTO 与 VO
DTO(Data Transfer Object)数据传输对象
传输的数据属性与实体差异大时,用 DTO 来封装
但在 service 层要新建实体对象来接受,可以拷贝属性
BeanUtils.copyProperties(employeeDTO, employee);VO(Value Object)值对象
用于封装返回给前端的数据,例如分页查询
Page<SetmealVO>,他要返回 setmeal 的一些属性和 categoryId 对应的 categoryName用 SetmealVO 封装用 setmeal 表左连接 categoryId 返回的需要的数据
lombok 插件
- @Data 自动生成 getter 和 setter
- @Builder 自动生成 builder 模式相关代码
- @Slf4j 用于自动创建日志对象,并把对象命名为 log
类上加注解就能用日志对象 log 了log.error("异常信息:{}", ex.getMessage());
@Configuration 和 @Bean
@Configuration修饰配置类,@Bean修饰配置类里面的方法将返回对象注册为 Bean- 配置类会在启动时加载,调用里面的所有方法,并把对象放到 Spring 容器中
- 比如注册拦截器,生成接口文档。拦截器是注入进来注册的,接口文档是生成的 Bean,所以配置类里面的拦截器方法不需要@Bean
@Component
@Component修饰类,标识类为一个组件,其他类可用@Autowired 注入- 比如从配置文件里封装的 jwt 令牌配置类,jwt 令牌校验类,公共字段填充类
@RestController
- @RestController 是 Spring 框架的注解
- 它是一个组合注解,相当于同时使用@Controller 和@ResponseBody,表示该类中的所有方法都会直接返回数据而不是视图名称,自动将返回值序列化为 JSON 等格式响应给客户端。
- 在前后端分离的项目,虽然仍用 MVC 这种模式,但后端就只负责处理数据不需要返回视图,所以这个项目所有控制器都用的这个注解修饰
@ResponseBody 修饰方法,表示该方法返回的数据会被转序列化后直接写入 HTTP 响应体中,而不是被解析为视图。
监控变量,修改变量,计算表达式
- debug 模式下,右键变量有 add to watches 添加监视器,能直接看变量的各种属性
- debug 模式,进行到哪步代码后面也会出现这一行对象的全部属性
- 右键对应属性,还可以动态修改
- 方法也可以计算,框选后右键可对表达式求值
- 这个别的 IDE 也有,实时监控各种变量的值都是什么,用来做算法题也非常好用
Swagger 生成接口文档和在线调试
- Knife4j 是一个 maven 依赖,是 JavaMVC 框架里的 Swagger
- 要在对应的 DTO,VO 类和属性写上注解才能被识到
@ApiModel(description = "员工登录时传递的数据模型")和@ApiModelProperty("用户名") - 扫描指定包下的接口,通过反射获取接口信息
localhost:8080/doc.html可以访问生成的接口文档,用后端项目端口访问,不是前端- 接口文档也能直接调试 api,类似 postman 和 apifox 也可以直接导入到他们里面
批量插入和删除
用 foreach 循环 collection 为传入的集合
1 | |
1 | |
全局异常处理器
捕获整个应用的异常统一处理,避免在每个控制器中重复编写异常处理代码
用
@ControllerAdvice注解修饰,@ExceptionHandler修饰方法放在 service 模块里面的 handler 文件夹里
前端请求到达 Controller 后,若执行顺利则调用 controller 里面的
return Result.success(orderSubmitVO);若发生异常,则会被全局异常处理器拦截并统一调用拦截器里面的
return Result.error(ex.getMessage());。result 是一个封装了 msg,code,data 的对象,用静态工厂方法构造
success(),success(T object),error(String msg)
异常处理
- 可以在方法上 throws Exception 抛出异常
- 方法内用 throw new Exception(“异常信息”)抛出异常
- throws / throw 抛出的异常是按调用栈会向上传递,从 ServiceImpl 到 Service 再到 Controller,最终会调用全局异常处理器
- try catch 会捕获异常,异常就会直接处理,不会向上传递
lambda 表达式
语法:对象::方法
将当前集合里面每个元素的 dishId 赋值为 dishIdflavors.forEach(dishFlavor -> dishFlavor.setDishId(dishId));一条语句可以省略掉 {}
1 | |
路径参数
- 是用 URL 路径,例如 GET /dish/1
- 要先在 mapping 上面加占位符
@GetMapping("/{id}") - 再在 controller 方法的参数上加
public Result<DishVO> getById(@PathVariable Long id) - 最终路径为
/dish/{id}
http 传输都是明文的,GET 用 URL 传数据,POST 用 body 传数据,抓个包都能看见,用 https 后传输的数据会被加密
query 参数
- 是出现在 URL 里
?后面的参数 - 例如分页查询 GET /products?category=electronics&page=2&limit=10
- controller 可以直接接收 query 参数例如
(int page, int pageSize, Integer status) - controller 也可以用 DTO 接受参数,Spring 会自动绑定字段
- 用
@RequestParam("key")修饰参数可以指定给对应形参传值,都同名的时候可以省略 - 也可以搭配路径参数一起使用,例如 GET /users/123/posts?status=published&sort=date
mybatis 查询的整个逻辑
- Controller → Service → ServiceImpl → Mapper 接口 → Mapper.xml 是标准的四层架构(表现层、业务层、持久层、数据库)
- Controller 注入的是 EmployeeService 接口,实际使用的是 EmployeeServiceImpl 实现类,这是 Spring 推荐的面向接口编程方式。
- DTO 用于接收前端参数,VO 用于返回给前端,Entity 是数据库实体
- mapper 是与其对应数据表库中的表的操作,所以多表查询是在 ServiceImpl 中调用多个 mapper 完成的
发出请求到 Controller
1
2
3
4
5
6
7
8// 注意之前要注入 Service 接口
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
// 请求进来后,在里面调用Service
Employee employee = employeeService.login(employeeLoginDTO);
// 返回员工后,封装成VO,最后返回给前端
return Result.success(employeeLoginVO);
}Controller 调用 Service 里的方法
1
2
3
4// 传递DTO给实现类
public interface EmployeeService {
Employee login(EmployeeLoginDTO employeeLoginDTO);
}Service 使用对应的 ServiceImpl 里的实现
1
2
3
4
5
6
7
8
9
10
11
12
13@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Override
public Employee login(EmployeeLoginDTO employeeLoginDTO){
// 调用mapper查询
Employee employee = employeeMapper.getByUsername(username);
// 后续有验证登录的逻辑,不通过抛出对应异常
return employee;
}
}ServiceImpl 调用 DAO/Mapper.java 接口
1
2
3
4
5
6@Mapper
public interface EmployeeMapper {
// 简单的查询可以直接用注解
// @Select("select * from employee where username = #{username}")
Employee getByUsername(String username);
}DAO/Mapper.java 接口调用 Mapper.xml 里面的 sql 实现
1
2
3
4
5
6
7// xml 文件里封装了实际使用的 SQL
// MyBatis 根据 XML 中的 SQL 执行数据库操作,并将结果映射为 Java 对象
<mapper namespace="com.sky.mapper.EmployeeMapper">
<select id="getByUsername" parameterType="string" resultType="com.sky.entity.Employee">
select * from employee where username = #{username}
</select>
</mapper>最后返回的逻辑
ServiceImpl 调用 Mapper 执行查询后返回数据库结果
Mapper.xml 封装成实体类,返回给 ServiceImpl
最后返回给 Controller,Controller 层返回给前端
mapper 接口的注解 sql
直接在 mapper 接口里写简单的 sql,就不用在 xml 文件里写实现了
末尾要留空格,因为实际的 sql 是这几行拼起来
1 | |
Repository 层
都是持久层(DAO 层),Repository 层可以替代 Mapper 层,一般二选一,这个项目用的是 Mapper 层
Mapper 技术用的是 MyBatis,Repository 层用 JPA(JAVA 持久化 API)
JPA 的使用
创建 Repository 接口
Spring 会自动扫描所有继承 Repository(或其子接口)的接口,并为每个接口创建动态代理对象
会根据方法名自动解析行为,最后将代理对象注入到 Service 中
1 | |
在 Service 或 Controller 中调用
1 | |
查询语句格式
方法名必须遵循这个结构[操作类型] + [字段名] + [条件关键词]
- 方法名以 find、read、query、get、count、exists 等开头(语义不同,但效果一样,推荐用 find)
- 后面跟 实体类的属性名(首字母大写)
- 可加 条件关键词:By、And、Or、Between、Like、IgnoreCase 等
- 区分大小写:属性名必须和 Entity 中的字段名一致(按 JavaBean 规范)
- 单条件查询
findByEmail(String email) - 多条件查询
findByEmailAndName(String email, String name) - 比较操作
剩下的操作就自己用ai去查吧 - 模糊查询
- 忽略大小写
- 排序
- 限制结果数量
- 统计&判断存在
查询太复杂时就不用约定了,如 JOIN、GROUP BY 等,方法名就会变得极长,甚至无法表达
这时候就用@Query自己写 JPQL/HQL 语句
1 | |
模糊查询
1 | |
PageHelper 分页查询
- 自动处理分页逻辑,只需调用 PageHelper.startPage(pageNum, pageSize),就会自动拦截并改写 sql
- 返回的 pageResult 对象包含两个属性,total 和 records
Controller 层
1 | |
Service 层
1 | |
ServiceImpl 层
1 | |
Mapper.java
1 | |
Mapper.xml
1 | |
AOP 面向切面编程
例如执行插入时需要创建时间,创建人
自定义注解(annotation),标记需要处理的方法
1
2
3
4
5
6
7// 表示当前注解用于方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
// 数据库操作类型: UPDATE INSERT
OperationType value();
}定义切面类(aspect),统一拦截加入注解的方法,然后用反射赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14@Aspect
@Component
@Slf4j
public class AutoFillAspect {
// 切点,在 mapper 包下,并且加入了 AutoFill 注解的方法
@Pointcut("execution(_ com.sky.mapper._.\*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {
}
// 前置通知,在切点方法执行前执行,在通知中为公共字段赋值
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段填充");
}
}- 最后在需要的 Mapper 方法上加上注解,注解会自动把参数里面的公共字段赋值,注意传参的对象要有这个属性来接受
- 然后在 xml 里面加上 set if 对应字段,注解是给对象赋值,最后要根据对象写完整的 sql
- 最好不同时用两个注解,@AutoFill 和@update 都进行了 sql 可能会冲突导致 AutoFill 失败,所以把那个简单的 sql 还是写在 xml
@AutoFill(value = OperationType.INSERT)void insert(Employee employee);
阿里云 OSS
pom 文件导入依赖
新建一个 OSS 存储空间,并获取访问密钥
在 yml 文件中配置 endpoint access-key-id access-key-secret bucket-name
给上面的属性封装成 AliOssProperties 类
最后新建一个工具类 AliOssUtils 处理文件上传
注解方式的事务管理
@Transactional功能,方法执行前自动开启事务,方法正常结束时提交事务,出现异常时回滚事务
一般涉及两个及以上表的数据库操作,要使用事务管理,确保数据一致性
要先在 Application 启动类加上 @EnableTransactionManagement 才能开启
ThreadLocal
- 每一次请求都是一个单独的线程,验证可以用
System.out.println("当前线程的ID:" + Thread.currentThread().getId()); - 同一个线程内可以用
ThreadLocal存取数据 BaseContext.setCurrentId(empId);在拦截器里存储当前用户的 ID,BaseContext 是放在 Common 里面的工具类
主键回显,获取自增的 id
mapper.xml 的标签里面加入后两个选项来打开回显和指定写回的字段<insert id="insert" useGeneratedKeys="true" keyProperty="id">
或者 mapper.java 的标签里面加入注解@Options(useGeneratedKeys = true, keyProperty = "id")
同时修改两张表时的操作
修改菜品时,修改他的口味的数据
先把原来的口味数据删除,再插入新改动后的数据
1 | |
用户端管理端 controller 重名,bean 冲突
- 在@RestController 后面加上别名,
@RestController("userShopController") - 生成接口文档的配置类在 new 的 docket 后面加.groupName(“管理端接口”),扫描的包名也改,之后接口文档就能按模块进行分组
JWT 令牌运作的整个流程
登录服务器存储一个私钥,在用户登陆成功后生成 jwt 令牌给用户
之后其他服务器存储一个统一的公钥,在收到请求时就会用公钥和 jwt 令牌前面的部分与数字签名进行验证
- 对称加密,所有服务器端都用同一个密钥进行签名验签
- 非对称加密,登录服务器生成 jwt 时用私钥签名,其他服务器用同一公钥进行验签
非对称加密的整个流程
- 首先用户发送用户名密码进行登录,服务器收到后在数据库中比对用户名密码
- 比对成功后服务器根据算法,用户的信息,和服务器存储的私钥生成 token 发回给用户端
- 用户端会将 token 存储在本地,每次请求都会携带这个 token 作为认证信息
- 之后服务器收到用户请求时,会验证 token 的合法性,通过共有的公钥生成签名与发送来的签名对比,如果一致则验证通过
- 在 token 过期或生成新的 token 之前,用户端会一直携带这个 token
token 结构
header.payload.signature
- header: 存放 token 的生成算法,token 的类型
- payload: 存放 token 的内容,比如用户信息(非用户敏感信息,如 id 等用于标识用户),过期时间,生成时间,随机数
- signature: 签名,根据服务器秘钥由 header 和 payload 生成
jwt 其他要点
- 因为秘钥存储在服务器,所以别人只拿到 header 和 payload,无法生成 signature,从而无法验证 token 的合法性
- jwt 解决了 session 服务器存储开销大和跨域/分布式支持困难的问题
- jwt 的解密在拦截类中进行(苍穹外卖定义在 interceptor 包下的 JwtInterceptor 类),用@Component 注解修饰,实现 HandlerInterceptor 接口,最后在 WebMvcConfiguration 类中注册自定义拦截器,拦截/排除指定路径
- token 的名称是约定,由前端带过来的 admin 端是
token,user 端是authentication,都写在application.yml文件中
如何使jwt主动失效
jwt是无状态的,客户端每次发过来服务端只有验签的能力,并不能清除登录状态
几种失效方案
- 黑名单方案:
点退出登录时,主动将jwt存入Redis,并设置过期时间
每次业务服务器收到请求时,除了验签,还要去Redis中查询是否在黑名单中,如果在就拒绝 - 短期Token + 刷新Token:
access token(短期token):有效期很短,几十分钟,用于请求接口
refresh token(刷新token):有效期长,一周、一个月,用于刷新 access token
用户请求时,先用refresh token 去获取 access token,之后的请求再用 access token 访问接口
之后access token 过期时,就用refresh token 去获取新的 access token,refresh token 过期时,用户需要重新登录 - 白名单方案:
和黑名单相反,就是存入每个用户的token,请求时,只有Redis中存在才放行 - 版本号/时间戳机制:
在token中加一个版本号,用户表中也加一个token版本号字段
用户登出时把版本号加1,服务器收到token时,解析出来里面的版本号,然后取数据库中查当前用户的版本号
如果用户的token版本号小于数据库中的就让当前的token失效
jwt内部包含着exp(有效期)字段,所以后端可以直接获取exp,判断是否过期,jwt因为有签名也无法伪造,不怕用户自己改exp
所以jwt自己到期就能失效,上面这些都是让jwt主动失效的手段,比如退出登录或异常登录管控
微信小程序登录过程
openid 是微信用户的唯一标识符
小程序点击登录后,用户端带着 code 到服务器
服务器拿到 code 后,调用微信的接口,获取 openid
服务器拿到 openid 后,生成 token,返回给用户端
redis 操作
先在 config 文件夹里创建 RedisConfiguration 类,返回 RedisTemplate 对象
之后在别的地方器里注入 RedisTemplate 对象,就可以操作 redis 了
1 | |
从 redis 中取出的数据是字符串,强制转换的类型取决去当初放进去时的类型List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);list = dishService.listWithFlavor(dish);redisTemplate.opsForValue().set(key, list);
增删改的操作都需要清理缓存
Spring Cache 缓存
是 Spring 提供的缓存框架,用注解开启缓存功能
@EnableCaching开启缓存功能,通常加在启动类上,例如 SkyApplication 类@Cacheable在方法执行前先查询缓存数据中是否有数据,如果有则直接返回不会调用方法,没有则执行方法,将方法返回值放入缓存中@CachePut将方法的返回值放入缓存横中@CacheEvict从缓存中删除数据
1 | |
查询缓存数据@Cacheable(cacheNames = "userCache", key = "#id")
查到就直接返回,没有就执行方法查数据库,将返回值放入缓存中
删除一条缓存@CacheEvict(cacheNames = "userCache", key = "#id")
删除所有缓存@CacheEvict(cacheNames = "userCache", allEntries = true)
查询操作加查询缓存注解
增删改操作加删除缓存
cpolar 内网穿透
可以获取一个临时的公网 ip,将他映射到 nginx 的 localhost:端口号,然后就能用 cpolar 提供的公网 ip 访问内网了
- 下载登录后进入到官网的验证界面复制 Authtoken
- 然后进入到安装目录打开 cmd 运行
cpolar.exe Authtoken 复制的Authtoken(只需做一次验证) - 最后 cmd 界面运行
cpolar.exe http 端口号就能获取一个临时的公网 ip
list 转换成字符串
.stream()转换成 stream 流.map(x -> {...})把每一个元素(x)映射成新的元素{…}.collect(Collectors.toList())将 stream 流转换成 list
1 | |
假设有两个对象
new OrderDetail(“宫保鸡丁”, 2)
new OrderDetail(“鱼香肉丝”, 1)
执行之后
orderDishList = [“宫保鸡丁2;”, “鱼香肉丝1;”]
百度地图 API 引入
百度地图的 API
左上角产品服务那里可以找到需要的 API
- 先去官网创建应用或取 AK 秘钥
- 将店铺地址 address 和 AK 秘钥配置在
application.yml文件中 - 然后在 ServiceImpl 层里引入这两个属性
@Value("${sky.baidu.ak}") - 在 ServiceImpl 层里新建一个私有方法用于校验距离范围
- 方法里面建立 map 对象用来传参,把店铺地址,AK,输出类型都放进去
- 然后用 HttpClientUtil 类以 map 作参数,发起 get 请求来调用百度地图的 API 接口
- 先获取返回的 json 数据,然后解析为经纬度,map 存储店铺和用户的经纬度坐标
- 再调用路线规划的 API,传两个坐标,返回距离等数据
- 最后用获取的距离判断是否超出配送范围
1 | |
Spring Task 任务调度管理
掌管定时任务
- 包含在 Spring-context 里
- 启动类添加 @EnableScheduling 开启任务调度
- 自定义定时任务类
cron 表达式
本质是字符串,分为六或七个域,由空格分割
秒 分 时 日 月 周 年
2020 年 10 月 12 日 9 小时 0 分 0 秒
0 0 9 12 10 ? 2020
可以用在线 Cron 表达式生成器
自定义定时任务类
1 | |
WebSocket
- 一种基于 TCP 的一种新的网络协议,实现客户端与服务器的双向通讯
- http 是 短连接,ws 是 长连接;http 是单向通讯,ws 是双向通讯
- 应用场景,弹幕,聊天窗口,实况数据推送
- 创建 WebSocketConfig 类,用@Configuration 和 @Bean 注解注册 ServerEndpointExporter
返回的实例会扫描所有使用 @ServerEndpoint 注解的类,并注册成 WebSocket 服务 - 创建 websocket 包,里面创建 WebSocketServer 类,用@Component @ServerEndpoint(“ws/{sid}”) 注解标识 WebSocket 服务
这个类里面用哈希表存放 session 并用@OnOpen @OnClose @OnMessage 等创建回调方法,以及群发的方法 - 在对应 ServiceImpl 里面注入 WebSocketServer 类,就可以新建哈希表放入约定的传参
把哈希表转换成 JSON (JSON.toJSONString(map))作为参数调用 WebSocketServer 类的群发方法
创建 ws 连接:
- 管理端在进入时就会发出 ws 请求,随机生成 sid,并把 sid 作为参数,发送给服务端
- 这个请求会被符合对应路径的这个注解方法拦截@ServerEndpoint(“/ws/{sid}”),这时候就建立连接了,会自动创建一个 session 对象,包含当前连接的所有相关信息
- 然后服务端会把 sid 作为 key,把 session 作为 value,保存在 map 中,发消息就会用这个 map 获取 session,然后调用 session.getBasicRemote().sendText(message)
Apache POI
一个开源的 Java API,用于读写 Microsoft Office Excel 文件。
用 Java 的 api 新建 sheet,row 对象,填充修改 resource 里面的 template 文件夹里的模版文件
用输出流输出到浏览器下载ServletOutputStream out = response.getOutputStream();
输出完后关闭资源(outStream 和 excel)
Spring 依赖注入
IOC(Inversion of Control,控制反转)
传统设计横中,类会主动创建他所依赖的对象
1 | |
用 IOC 之后,对象的控制权反转给 Spring 容器,由 Spring 容器创建对象,并注入给 UserService 类
推荐用构造器注入(如果只有一个构造函数,可以省略 @Autowired)
其次是用 setter 注入(都要加@Autowired 注解)
字段注入加@Autowired 的方式不推荐
1 | |
依赖注入的类要启动时就注册为 Bean 才能被 Spring 容器管理
所以要有@Component、@Service、@Mapper、@Repository 、@Bean、@Mapper 注解
做完了,接下来的打算
背八股文,java mysql spring redis 还有消息队列,分布式,网络协议,操作系统都去背一遍吧
然后还要继续刷算法
再练练打字