苍穹外卖笔记
基础笔记
- 接口文档可以导入到 apifox 方便查看
@RequestBody是接受前端的 json 数据,然后反序列化为 Java 对象Result<T>封装了返回给前端的数据,code,message,data
DTO 与 VO
DTO(Data Transfer Object)数据传输对象
传输的数据属性与实体差异大时,用 DTO 来封装
但在 service 层要新建实体对象来接受,可以拷贝属性
BeanUtils.copyProperties(employeeDTO, employee);VO(Value Object)值对象
用于封装返回给前端的数据,例如分页查询 Page
,他要返回 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 等格式响应给客户端。
监控变量,修改变量,计算表达式
- debug 模式下,右键变量有 add to watches 添加监视器,能直接看变量的各种属性
- debug 模式,进行到哪步代码后面也会出现这一行对象的全部属性
- 右键对应属性,还可以动态修改
- 方法也可以计算,框选后右键可对表达式求值
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 | |
模糊查询
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 | |
- 定义切面类(aspect),统一拦截加入注解的方法,然后用反射赋值
1 | |
- 最后在需要的 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 令牌运作的整个流程
- 首先用户发送用户名密码进行登录,服务器收到后在数据库中比对用户名密码
- 比对成功后服务器根据算法,用户的信息,和服务器存储的唯一秘钥生成 token 发回给用户端
- 用户端会将 token 存储在本地,每次请求都会携带这个 token 作为认证信息
- 服务器收到用户请求时,会验证 token 的合法性,通过秘钥生成签名与发送来的签名对比,如果一致则验证通过
- 在 token 过期或生成新的 token 之前,用户端会一直携带这个 token
token 结构: header.payload.signature
header: 存放 token 的生成算法,token 的类型
payload: 存放 token 的内容,比如用户信息(非用户敏感信息,如 id 等用于标识用户),过期时间,生成时间,随机数
signature: 签名,根据服务器秘钥由 header 和 payload 生成
因为秘钥存储在服务器,所以别人只拿到 header 和 payload,无法生成 signature,从而无法验证 token 的合法性
jwt 解决了 session 服务器存储开销大和跨域/分布式支持困难的问题
jwt 的解密在拦截类中进行(interceptor 包下的 JwtInterceptor 类),用@Component 注解修饰,实现 HandlerInterceptor 接口,最后在 WebMvcConfiguration 类中注册自定义拦截器,拦截/排除指定路径
token 的名称是约定,由前端带过来的 admin 端是
token,user 端是authentication,都写在application.yml文件中
微信小程序登录过程
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
1 | |
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 类
推荐用构造器注入其次是用 setter 注入,@Autowired 不推荐
1 | |
依赖注入的类要启动时就注册为 Bean 才能被 Spring 容器管理
所以要有@Component、@Service、@Mapper、@Repository 、@Bean、@Mapper 注解
做完了,接下来的打算
背八股文,java mysql spring redis 还有消息队列,分布式,网络协议,操作系统都去背一遍吧
然后还要继续刷算法
再练练打字