Intro

只是浅尝,
对于这门课而言,有点像搭积木了,
Java拼好码么?
那很有生活了。
但是一些工程化的思想却流入了我的大脑,
让我在余下的时间思考工程化的意义。

课程准备

环境配置

前端环境配置

整个课程已经提供了Windows环境下的Nginx-1.20.2软件包,且其中已经包含了前端文件,
所以直接启动Nginx服务就行。
Nginx涉及配置也已经提供以及涉及到的负载均衡等高级问题,这个这段还无需关心。

后端环境配置

IDEA直接从官网下了最新版,反正我的账号已经学生认证,不用白不用。
配置项目的时候犯了点傻,下了最新版JDK,导致后面尝试启动项目的时候发生错误。
课程好像没讲用的什么JDK,不过研究了一下确定了应该使用JDK 8。

技术栈

Web框架:SpringBoot

SpringBoot 人如其名,就是“快速启动”的Spring
SpringBoot 按照 “约定大于配置” 的原则,直接为开发者设置好了配置。

Spring 是什么?

Spring主要做的就是两件事情:

  1. 生成Bean(扫描有什么Bean,并在需要的时候生成)
  2. 管理Bean(管理Bean生命周期)

当然,这些底层如何实现就要涉及到 反射等 高级特性了,这里不关心。

DB:MySQL

课程使用的是MySQL,但是没有具体提到版本。我用的是5.7。
因为事先接触过Docker技术,所以果断选择了wsl环境下拉了个mysql5.7的镜像,并挂载conf和课程提供的sql文件生成镜像。
后面调试的时候发现中文发生了乱码,因为以前遇见过类似错误,所以很快锁定到,是编码集的问题。
一查果然是,遂重新以utf-8的格式进行编码数据库。
这里偷了个懒,直接删了镜像重新创建了。

  • 发生这等错误的缘故好像是因为MySQL5.7默认编码集不是utf-8的问题

缓存:Redis

Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件
主要特点:

  • 基于内存存储,读写性能高  
  • 适合存储热点数据(热点商品、资讯、新闻)
  • 企业应用广泛

Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。它存储的value类型比较丰富,也被称为结构化的NoSql数据库。

NoSql(Not Only SQL),不仅仅是SQL,泛指非关系型数据库。NoSql数据库并不是要取代关系型数据库,而是关系型数据库的补充。

微信小程序

小程序是一种新的开放能力,开发者可以快速地开发一个小程序。可以在微信内被便捷地获取和传播,同时具有出色的使用体验。

首先,在进行小程序开发时,需要先去注册一个小程序,在注册的时候,它实际上又分成了不同的注册的主体。我们可以以个人的身份来注册一个小程序,当然,也可以以企业政府、媒体或者其他组织的方式来注册小程序。那么,不同的主体注册小程序,最终开放的权限也是不一样的。比如以个人身份来注册小程序,是无法开通支付权限的。若要提供支付功能,必须是企业、政府或者其它组织等。所以,不同的主体注册小程序后,可开发的功能是不一样的。

然后,微信小程序我们提供的一些开发的支持,实际上微信的官方是提供了一系列的工具来帮助开发者快速的接入
并且完成小程序的开发,提供了完善的开发文档,并且专门提供了一个开发者工具,还提供了相应的设计指南,同时也提供了一些小程序体验DEMO,可以快速的体验小程序实现的功能。

最后,开发完一个小程序要上线,也给我们提供了详细地接入流程。

微信登录流程

步骤分析:

  1. 小程序端,调用wx.login()获取code,就是授权码。
  2. 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
  3. 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
  4. 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
  5. 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
  6. 小程序端,收到自定义登录态,存储storage。
  7. 小程序端,后绪通过wx.request()发起业务请求时,携带token。
  8. 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
  9. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。

微信支付

前面的课程已经实现了用户下单,那接下来就是订单支付,就是完成付款功能。支付大家应该都不陌生了,在现实生活中经常购买商品并且使用支付功能来付款,在付款的时候可能使用比较多的就是微信支付和支付宝支付了。在苍穹外卖项目中,选择的就是微信支付这种支付方式。

要实现微信支付就需要注册微信支付的一个商户号,这个商户号是必须要有一家企业并且有正规的营业执照。只有具备了这些资质之后,才可以去注册商户号,才能开通支付权限。

个人不具备这种资质,所以我们在学习微信支付时,最重要的是了解微信支付的流程,并且能够阅读微信官方提供的接口文档,能够和第三方支付平台对接起来就可以了。

参考:https://pay.weixin.qq.com/static/product/product_index.shtml

完成微信支付有两个关键的步骤:

第一个就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。

第二个就是支付成功之后微信后台会给推送消息。

这两个接口数据的安全性,要求其实是非常高的。

解决: 微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。

在后绪程序开发过程中,就会使用到这两个文件,需要提前把这两个文件准备好。

工具库

MyBatis

用于处理数据库映射的工具,在 XML 下编写SQL。
本质上也是个参数化查询的工具罢了。

Swagger

一个WebGUI文档管理界面。
依赖 knife4j 框架自动扫描API端点,以及附着的注释,
从而生成对应的API文档。
并提供强大的API端点测试功能。

HttpClient

HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
HttpClient的核心API:

  • HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
  • HttpClients:可认为是构建器,可创建HttpClient对象。
  • CloseableHttpClient:实现类,实现了HttpClient接口。
  • HttpGet:Get方式请求类型。
  • HttpPost:Post方式请求类型。

HttpClient发送请求步骤:

  • 创建HttpClient对象
  • 创建Http请求对象
  • 调用HttpClient的execute方法发送请求

Spring Data Redis

我们在java程序中应该如何操作Redis呢?这就需要使用Redis的Java客户端,就如同我们使用JDBC操作MySQL数据库一样。

Redis 的 Java 客户端很多,常用的几种:

  • Jedis
  • Lettuce
  • Spring Data Redis

Spring 对 Redis 客户端进行了整合,提供了 Spring Data Redis,在Spring Boot项目中还提供了对应的Starter,即 spring-boot-starter-data-redis。

Spring Data Redis 是 Spring 的一部分,提供了在 Spring 应用中通过简单的配置就可以访问 Redis 服务,对 Redis 底层开发包进行了高度封装。在 Spring 项目中,可以使用Spring Data Redis来简化 Redis 操作。

Spring Cache

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:

  • EHCache
  • Caffeine
  • Redis(常用)

在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。

Spring Task

Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

定位: 定时任务框架

作用: 定时自动执行某段Java代码

应用场景:
1). 信用卡每月还款提醒
2). 银行贷款每月还款提醒
3). 火车票售票系统处理未支付订单
4). 入职纪念日为用户发送通知

只要是需要定时处理的场景都可以使用Spring Task

WebSocket

WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。

HTTP协议和WebSocket协议对比:

  • HTTP是短连接
  • WebSocket是长连接
  • HTTP通信是单向的,基于请求响应模式
  • WebSocket支持双向通信
  • HTTP和WebSocket底层都是TCP连接

思考: 既然WebSocket支持双向通信,功能看似比HTTP强大,那么我们是不是可以基于WebSocket开发所有的业务功能?

WebSocket缺点:

服务器长期维护长连接需要一定的成本
各个浏览器支持程度不一
WebSocket 是长连接,受网络限制比较大,需要处理好重连

结论: WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用

WebSocket应用场景:
1). 视频弹幕
2). 网页聊天
3). 体育实况更新
4). 股票基金报价实时更新

Apache POI

Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。
一般情况下,POI 都是用于操作 Excel 文件。

Apache POI 的应用场景:

  • 银行网银系统导出交易明细
  • 各种业务系统导出Excel报表
  • 批量导入业务数据

Apache POI既可以将数据写入Excel文件,也可以读取Excel文件中的数据。

学习

基本业务流程设计

数据是如何在不同层级间流动的

图示

案例

从源码入手:
参考 Employee 相关业务流程

  1. -> public class EmployeeController
    1
    2
    3
    4
    5
    6
    7
    @PutMapping  
    @ApiOperation("编辑员工信息")
    public Result update(@RequestBody EmployeeDTO employeeDTO){
    log.info("编辑员工信息:{}", employeeDTO);
    employeeService.update(employeeDTO);
    return Result.success();
    }

可以看到,当前数据来到了 Controller层 的 update方法。
update方法 从 请求体 中 提取出数据并包装为 EmployeeDTO,
然后派发给 Service 层 进行业务逻辑处理。

  1. -> public class EmployeeServiceImpl implements EmployeeService
    1
    2
    3
    4
    5
    6
    7
    public void update(EmployeeDTO employeeDTO) {  
    Employee employee = new Employee();
    BeanUtils.copyProperties(employeeDTO, employee);


    employeeMapper.update(employee);
    }

在 Service层 中, 数据被提取并映射为相应实体,
并交由 Mapper 层进行持久化处理,这里不再演示。

类似查询之类的逻辑也是一样的,
只不过变成了:
Controller -> Service -> Mapper -> Service -> Controller

缓存设计

图示

案例

public class ShopController

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/status")  
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
if (status == null) {
log.warn("从Redis中未获取到店铺营业状态,默认返回打烊中");
status = 0;
} else {
log.info("获取到店铺的营业状态为:{}", status == 1 ? "营业中" : "打烊中");
}
return Result.success(status);
}

可以看到,直接在 Controller层 操作 redis连接对象 进行查询,
为了避免异常,对可能发生的错误提前包裹一下判断。

改动

图片改为本地存储

不想使用 阿里云OSS 所以改成本地存储了

一、 设置静态资源映射

1
2
3
4
5
6
7
8
9
/**  
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
// ...
}

二、 修改文件上传接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@RestController  
@RequestMapping("/admin/common")
@Slf4j
@Api(tags = "通用接口")
public class CommonController {

@Autowired
private LocalStorageUtil localStorageUtil;

@Value("${sky.local.storage.access-url:http://localhost:8080/static}")
private String accessBaseUrl;

/**
* 文件上传接口
* @param file 上传的文件对象
* @return 文件访问路径(成功时)/错误信息(失败时)
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(@RequestParam("file") MultipartFile file) {
// TODO
String originalFilename = file.getOriginalFilename();
log.info("收到本地存储请求,文件名: {}", originalFilename);

try {
// 1. 生成唯一文件名
String extension = StringUtils.getFilenameExtension(originalFilename);
String storedFileName = UUID.randomUUID() + "." + (extension != null ? extension : "");

// 2. 保存到本地
String relativePath = localStorageUtil.store(file.getBytes(), storedFileName);

// 3. 生成访问URL
String accessUrl = accessBaseUrl + "/" + relativePath.replace("\\", "/");

return Result.success(accessUrl);
} catch (IOException e) {
log.error("本地文件存储失败 - 文件名: {}", originalFilename, e);
return Result.error(MessageConstant.UPLOAD_FAILED);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}

三、 实现辅助工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component  
@Slf4j
public class LocalStorageUtil {

@Value("${local.storage.base-path:static}") // 默认为 static 目录下
private String basePath;

public String store(byte[] bytes, String fileName) throws IOException, URISyntaxException {
String dateDir = LocalDate.now().toString().replace("-", "/");

// 获取 classpath:/static 的路径
URL staticUrl = this.getClass().getClassLoader().getResource("static");
if (staticUrl == null) {
throw new IOException("未找到 static 资源目录");
}

// 使用 URI 构造路径,兼容 Windows
Path staticPath = Paths.get(staticUrl.toURI());
Path targetPath = staticPath.resolve(Paths.get(dateDir, fileName));

Files.createDirectories(targetPath.getParent());
Files.write(targetPath, bytes);

return Paths.get(dateDir, fileName).toString();
}
}