苍穹外卖笔记
基础笔记
- 接口文档可以导入到 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文件中
微信小程序登录过程
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 还有消息队列,分布式,网络协议,操作系统都去背一遍吧
然后还要继续刷算法
再练练打字