苍穹外卖超全超详细技术总结(适用于还没做项目、刚做完项目、技术学习)
前言
本文章是学习苍穹外卖项目后,基于项目用到的技术点进行的自我总结。适用于刚接触该项目的初学者们,可以跟着视频课同步使用本文章,遇到不理解的知识点了,可以用本文章进行查询回顾,建议收藏!本文章同样也适用于刚学完苍穹项目的入门者们,进行自我总结,查漏补缺,由于我也是个初学者,文章中如有出错,欢迎大家指出错误,我愿意虚心求教并改正错误。诚心希望这篇文章能对你的学习有所帮助。
!PS:如有项目辅导、bug答疑需要,加文末V,可远程。!
技术选型
技术选型:项目中使用到的技术框架和中间件等
1)什么是技术框架?
Java 技术框架是指在 Java 开发中广泛使用的一系列库、工具和结构化代码模式,这些框架为开发者提供了解决常见问题的通用方案。框架通常包括已经实现的模块、组件和工具集,可以减少开发时间、提高代码质量,并确保项目的一致性和可维护性。
2)什么是中间件?
Java 中间件是位于操作系统和应用程序之间的独立软件层,它为分布式应用程序提供通信、数据管理、事务处理、安全性、消息传递等服务。中间件通常用于简化应用程序之间的交互、数据传输和系统集成,常见的 Java 中间件包括应用服务器、消息队列、缓存系统、服务总线等。
下图为项目所涉及的技术选型:
下图为项目整体结构:
前端又分为管理端(Web)和用户端(小程序),但由于我主要是学习 Java 后端的,所以前端的知识了解的比较少,重点倾向于后端服务。后端就是基于 spring boot 开发的 Java 后端服务。
前端–管理端
Nginx
前端–管理端是基于 Nginx 服务器反向代理来运行的,通过反向代理可以将前端请求转发到后端服务。这里会有疑问了,什么是 Nginx?什么是 Nginx 反向代理?
1)什么是Nginx?
Nginx 是一个高性能的 HTTP 服务器和反向代理服务器,同时也是一个 IMAP/POP3/SMTP 代理服务器。
1.正向代理(Forward Proxy)
正向代理是客户端的代理,客户端通过代理服务器向目标服务器发送请求,并将返回的响应转发给客户端。放到实际应用中来举例,翻墙一般会用到正向代理,这里就不过多赘述了。
2.反向代理 (Reverse Proxy)
反向代理是服务器端的代理,它接收客户端的请求,然后将请求转发给后端的多个服务器并返回结果。反向代理增强了系统的安全性和负载均衡能力,提高了访问速度。本项目的前端–管理端就是基于 Nginx 服务器反向代理来运行的。举项目中的登录接口来举例。
前端发送的请求地址为:http://localhost/api/employee/login,
而我们后端的接口地址则为:http://localhost:8080/admin/employee/login,
可以很明显地看到前端请求地址和后端接口地址并不一样,但是却能请求成功,这就是因为通过了 Nginx 的反向代理,浏览器也就是我们用户,发出的请求先是请求到 Nginx 服务器,由 Nginx 反向代理把这个请求转发给后端的 tomcat 服务器。可能有人要问了,“那这不纯纯多此一举嘛,我直接发给 tomcat 不行嘛,为啥非要中间多这么个步骤?” 这就要说到 Nginx 反向代理的好处了。
3.反向代理的好处
3.1.进行负载均衡
负载均衡可以把大量的请求按着我们自己指定的方式均衡地分配给每台服务器,如下图所示,通过 Nginx 负载均衡,可以将前端发送的请求均匀地分发给服务A、B、C。如果没有 Nginx 负载均衡,就需要前端自己去访问后端的服务器,而前端只能固定的访问某一台服务器,效率就会大大折扣了。
3.2.保证后端服务安全
上面说到如果没有 Nginx 负载均衡,就需要前端自己去访问后端的服务器,但实际在我们实际企业项目的开发中,类似于上图的服务A、B、C是不会直接暴露出来的,因此,由前端是无法直接请求到后端服务的,只能通过 Nginx 这个入口。
3.3.提高访问速度
在请求到 Nginx 的时候,Nginx 会做一个缓存,如果我们请求一个之前请求过的接口地址,Nginx 就无须再请求真正的后端服务,而是会直接将缓存的数据响应给前端,访问速度会进一步的提高。
既然 Nginx 有那么多的优点,那我们该怎么去配置 Nginx 反向代理和 Nginx 负载均衡呢
4.配置反向代理
还是刚才登录的接口,由于请求路径中含有 “/api/” 当请求发到 Nginx 之后,Nginx 就会通过反向代理转发给后端 tomcat,而转发地址就是我们指定的地址,而后面那段动态的请求路径,也会追加到我们指定的地址后面,从而拼成一个完成的请求路径,而这个路径就正好对应了我们后端的接口地址,因此可以请求成功。
5.配置负载均衡
请求路径中如果含有 “/api/” ,还是通过 “procy_pass” 指令,由负载均衡平均的转发到后台的多台服务器,本项目就一个本地服务器,所以下面那个服务器我 # 备注起来了,不过肯定看到代码又有人要好奇了 “欸?服务器地址我能理解,后面那个weight是什么东西啊?体重?” (其实真的理解为体重的话也方便理解哈,再多自以为是一点儿,可以看到我一个weight赋了90,一个赋了10,可以把他们理解为一个胖子,一个瘦子,发出的请求是给Nginx负载均衡的饭,这个时候Nginx负载均衡就不会平均去分配了,它看一个胖子,一个瘦子,它会给胖子多一点儿饭,给瘦子少一点儿),这其实就是 Nginx负载均衡策略,如下图所示:
Apache ECharts
Apache ECharts 是一款基于 Javascript 的数据可视化图库表,提供直观、生动、可交互、可个性化定制的数据可视化图表。使用 Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。
前端–用户端
微信小程序
一种新的开放能力,可以在微信内被便捷地获取和传播,同时具有出色的使用体验。那么开发微信小程序之前需要做哪些准备呢?
开发微信小程序所要做的准备
1.注册小程序
2.完善小程序信息
3.下载开发者工具
小程序登录
后端
HttpClient
因为本项目的用户端是以小程序的模式呈现的,就需要我们在 Java 程序当中来构造请求,并且来发送请求。这里就需要使用到一项后端技术:HttpClient,那么什么是 HttpClient 呢?
1)什么是HttpClient?
HttpClient 是 Apache Jakarta Common 下的子项目,是一种用于发送 HTTP 请求和接收响应的客户端编程工具包。它通常用于与基于 HTTP 协议的服务进行交互。可以在Java程序中通过 HttpClient 工具包来构造http请求,并且发送 http 请求,那么要在程序中使用 HttpClient 这个工具包,我们需要做什么呢。
2)如何使用HttpClient?
1.导入maven坐标
本项目中因为已经导入了 aliyun-sdk-oss,而其底层使用到了 HttpClient,就不需要我们再去导坐标了
导入 jar 包后我们就要使用它发送请求啦
2.创建HttpClient对象
3.创建Http请求对象
4.调用HttpClient的execute方法发送请求
本项目已经帮我们创建好了一个 HttpClient 工具类,需要使用到的时候直接调用就行
MD5加密
在导入的项目初始代码中,员工密码在数据库表中是明文存储的,安全性是比较低的,可以使用MD5加密方式对明文密码加密来解决这一问题,那么什么是MD5加密呢?
1)什么是MD5加密?
MD5信息摘要算法(英文:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值,用于确保信息传输完整一致。
注意:MD5加密方式不可逆,如果要想比对密码,只能将一个明文密码通过MD5加密后,对比加密后的密文。
2)如何使用MD5加密完善登录功能?
本项目中使用MD5加密用于完善登录功能 ,操作步骤为:
1.修改数据库中明文密码,改为MD5加密后的密文
2.修改Java代码,将前端提交的密码进行MD5加密后再跟数据库中密码比对
Swagger
由于本项目是基于前后端分离的方式来开发的,这种开发方式就需要我们在开发前就先将接口定义好,然后前后端开发人员才能并行开发,接口设计是很需要我们去学习,去掌握的,但本项目由于时间原因,就直接导入了所有的接口,在后续的开发中,老师在课上也会带着详细地去分析每一个对应的接口是怎么确定下来的,这里就不过多赘述了。后端开发人员根据接项目需求和接口文档开发完代码后,要如何去验证开发的代码是否正确呢?我们就需要进行测试,在 JavaWeb 课程中我们用到 Postman 测试我们后端编写的接口,但是有些接口可能需要的参数非常多,再用 Postman 来测试,就需要构造特别多的参数,测试效率就会很低。那么如何使测试效率变高呢?这里就用到了 Swagger 技术,那么什么是 Swagger 呢?
1)什么是Swagger?
使用 Swagger 只需要按照它的规范去定义接口及接口相关信息,就可以做到生成接口文档,以及在线接口调试页面。直接使用 Swagger 是很繁琐的,因此项目中我们使用到的是 knife4j 框架。什么是 knife4j 呢?
2)什么是knife4j?
knife4j 是为 Java MVC 框架集成 Swagger 生成 Api 文档的增强解决方案,那么如何使用 knife4j 呢?
3)如何使用knife4j?
1.导入maven坐标
2.在配置类中加入knife4j相关配置
3.设置静态资源映射,否则接口文档页面无法访问
因为项目的表现层用的是 Spring MVC 框架,如果不设置静态资源,访问接口文档的时候是访问不到的
都配置好了就可以进行测试了
可以看到我们设置的配置是成功运行了,接下来就可以去浏览器中访问一下接口文档啦
4)Swagger常用注解
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,下图是 Swagger 常用注解
JWT
1)什么是JWT?
1.概念
JWT 全称:JSON Web Token,定义了一种简洁的、自包含的格式,用于在通信双方以 JSON 数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。说 JWT 简洁是因为 JWT 就是一个简单的字符串,可以在请求参数或者是请头中,直接来传递 JWT 令牌,而自包含呢指的就是 JWT 令牌看似是一个随机的字符串,但是呢我们是可以根据自身的需要在 JWT 令牌当中来存储自定义内容的,比如可以直接在 JWT 令牌中来存储用户相关的信息。相较于传统的会话跟踪方案,令牌技术有更多的优点,以及更高的安全性。
2.JWT令牌的组成
整个 JWT 令牌是由三个部分组成的,三个部分之间只用 “.” 来分隔
第一部分
是头部区域,其记录的内容比较固定,是令牌的类型以及签名算法,它的数据格式就是JSON数据格式。
上面大括号中是原始的 JSON 数据格式,alg 指定的就是签名算法,这里指定的算法就是 HS256,将来就会根据这里指定的算法对令牌进行数字签名,type 指定的就是令牌的类型,这里令牌的类型是 JWT。那么是如何由上面原始的 JSON 数据格式变成下面这样的字符串的呢?在生成 JWT 令牌的时候,会对上面原始的 JSON 数据格式的字符串进行一次 Base64编码,那么什么是 Base64 呢?
Base64
第二部分
第二部分是有效载荷,可以携带一些自定义信息和一些默认信息。
与头部区域一样,原始数据依旧是 JSON 数据格式的字符串,进行 Base64编码后变成下面这样的字符串。
第三部分
第三部分是签名,签名的目的是防止 Token 被篡改,从而确保令牌的安全性,而这个签名呢,正是通过第一部分头部区域所指定的签名算法所算出来的。并不是 Base64编码,而且还会融入前面第一部分和第二部分的内容。
现在能理解为什么 JWT 是用于在通信双方以 JSON 数据格式安全的传输信息的了吧,因为原始的数据格式就是 JSON 数据格式的。
2)如何生成JWT令牌?
1.导入maven坐标
2.生成JWT令牌
3.校验JWT令牌
4.注意事项
本项目已经帮我们创建好了一个 JWT 工具类,就无需我们再去编写这部分的代码了,也可以删除自行编写练习。
过滤器Filter
1)什么是过滤器Filter?
过滤器Filter,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一,过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能,过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理等。
2)如何使用过滤器Filter?
1.定义Filter
定义一个类,实现 Filter接口,并重写其所有方法
2.配置Filter
Filter类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
在 Filter类上加 @WebFilter 注解后,还需要在启动类上也加上一个注解,@ServletComponentScan,因为 Filter 是 JavaWeb 三大组件之一,并不是 spring boot 当中提供的,而如果想要在 spring boot 项目中使用 JavaWeb 三大组件就必须要在启动类上加上@ServletComponentScan 注解,加上该注解后就表示当前项目是支持 Servlet 相关组件的。
已经配置好了过滤器Filter,那么它的执行流程是什么样的呢?
3.过滤器Filter执行流程
4.过滤器Filter拦截路径
过滤器Filter 可以根据需求,配置不同的拦截资源路径,上面我们配置过滤器Filter 时,拦截的是 ”/*“ ,也可以自己配置自己所需的。常见的配置方式有如下三种:
补充:3)登录校验Filter
拦截器Interceptor
1)什么是拦截器Interceptor?
拦截器Interceptor 是一种动态拦截方法调用的机制,类似于过滤器。是由 Spring 框架提供的,用来动态拦截控制器方法的执行,在指定的方法调用前后,根据业务需要执行预先设定的代码。
2)如何使用拦截器Interceptor?
1.定义Interceptor
定义一个类,实现 Interceptor接口,并重写其所有方法
2.配置Interceptor(注册Interceptor)
3.拦截器Interceptor执行流程
4.拦截器Interceptor拦截路径
拦截器Interceptor 可以根据需求,配置不同的拦截路径,上面我们配置 拦截器Interceptor时,拦截的是 ”/**“ ,也可以自己配置自己所需的。
补充:3)登录校验Interceptor
和 过滤器Filter的登录校验过程是一样的
异常处理
出现异常,该如何处理?
方案一:在 Controller 的方法中进行 try…catch 处理
方案二:全局异常处理
如果采用方案一太麻烦了,本项目中有特别多的 Controller 方法,而且每个 Controller 中也有着许多的接口方法,每一个都进行 try…catch 处理就不优雅了,而采用方案二的话,不管是在 Controller层、Service层还是 Mapper层,只要出现了问题,都会统一的抛给全局异常处理器来处理,代码就会呈现更加优雅了。
package com.sky.handler;
import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.xmlbeans.impl.xb.xsdschema.Public;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
/**
* 处理SQL异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
}
分层解耦
上面提到 “不管是在Controller层、Service层还是Mapper层…” ,出现这些层的目的是为了实现分层解耦。首先先来介绍两个概念——内聚:软件中各个功能模块内部的功能联系
耦合:衡量软件各个层/模块之间的依赖、关联的程度
而在软件设计当中,有一个原则:高内聚低耦合。下图为耦合状态,即层与层之间相互依赖,牵一发而动全身。
下图为解耦后,此时,即使service层代码发生变动,也不会影响到controller层和dao层
那么解耦开后,各个层之间如果再要调用该怎么办呢?这就涉及到了spring当中的两个重要的概念,IOC和DI。那么什么是IOC和DI呢?
1)什么是IOC?
控制反转:Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。这是spring框架的第一大核心。原来在应用程序中需要什么对象就new什么对象,现在不需要了,现在是把所有的对象都交给了容器来统一管理。反转之前是由应用程序自身来控制对象的创建,而反转之后是由容器来控制,这里面提到的容器也被称作IOC容器或spring容器,这就是反转控制的含义。
2)什么是DI?
依赖注入:Dependency Injection,简称DI。容器为应用程序提供运行时所需要的依赖,称为依赖注入。
3)什么是Bean对象?
IOC容器中创建、管理的对象,称为bean。
4)解耦步骤
这些基本概念已经解释清楚了,接下来就要进行解耦的操作了。在本项目中,我们首先要先将Service层和Mapper层的实现类,交给IOC容器管理,然后在为Controller及Service注入运行时所依赖的对象,就完成了。
1.在Service层和Mapper层的实现类上加上 @Component 注解
2.为Controller及Service注入运行时所依赖的对象,即在成员变量上加上注解 @Autowired
而当注入对象时,可能会出现多个类型相同的bean,就会报错,解决办法如下:
@Primary 想要哪个bean生效,就在其上面加上一个 @Primary 注解
@Qualifier 在使用 @Autowired 注入bean对象时,配合着 @Qualifier 注解来指定要注入的bean对象
@Resource 使用此注解注入bean对象时就不需要再写 @Autowired 注解了,而是直接用 @Resource 注解来指定要注入的bean对象就行
@Resource 是由JDK提供的注解,默认是按照名称注入的。而@Autowired则是由spring框架提供的注解,默认是按照类型注入。
在spring框架中,除了 @Component 注解外,为了更好的标识web应用程序中bean对象到底属于哪一层又提供了 @Component 的三个衍生注解,如下图所示:
要想这四个注解生效,需要被组件扫描注解 @ComponentScan 扫描到。但这种方法是不推荐的,推荐的是按照 spring boot 项目的规范,将所写的代码,全部放在启动类所在包及其子包下,这样 spring boot 项目在启动的时候,就会自动扫描到这些 bean对象。如下图:
AOP
1)什么是AOP?
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于在不改变原有代码的情况下,通过“切面”(Aspect)来增强或修改程序的行为。它通过将横切关注点(如日志记录、安全性、事务管理等)从核心业务逻辑中分离出来,减少代码的重复性,提高代码的可维护性。
2)AOP的核心概念
切面(Aspect):包含横切关注点逻辑的模块,例如日志功能或安全性校验。
连接点(Join Point):程序执行过程中可以插入切面的点,比如方法的调用或字段的访问。
切入点(Pointcut):定义了在哪些连接点应用切面逻辑的表达式。
通知(Advice):在特定连接点执行的操作,通常与切入点相关联。通知有不同类型,如前置通知、后置通知、异常通知等。
3)如何使用AOP?
在本项目中,有许多业务表都有几个公共字段,比如员工表和分类表,这两张表中,都包含了这四个字段,如下图所示:
当我们向这些表中插入数据的时候,就需要为这些字段来赋值,如果每一个业务都要这么去赋值的话,代码就会冗余,而且不方便我们后期去维护,如何解决这些问题呢?首先需要明确一下这些字段分别是在什么时候去操作的,如下表所示:其中insert:插入操作,update:更新操作
整体的思路就是通过 AOP切面来统一处理,比如说要调用持久层的某个 Mapper,来进行一个 insert操作,这个时候就可以通过切面来统一拦截 Mapper这一层,来为我们的这几个公共字段进行赋值。这个时候又存在一个问题,这个持久层中并不是每个方法都需要去拦截然后赋值,比如进行一个查询方法,显然是不需要上面这几个公共字段的,这时候就需要有一种手段,能够知道当前这个持久层的操作需不需要来为这几个公共字段进行赋值。这种手段就是——定义注解。为 Mapper 的方法来加入注解,加入注解就表示当前方法需要被拦截,需要为这几个公共字段进行赋值,如果没有加注解,就不需要处理。具体方法如下:
1.自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
2.自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill注解的方法,通过反射为公共字段赋值
3.在 Mapper 的方法上加入 AutoFill注解
具体实现代码如下:
自定义注解代码:
package com.sky.annotation;
import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
自定义切面类代码:
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MemberSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* 自定义切面类:实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知,在通知中进行公共字段赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段的自动填充...");
//获取到当前被拦截到的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
OperationType operationType = autoFill.value();//获得数据库操作类型
//获取到当前被拦截到的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if ( args == null || args.length == 0){
return;
}
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的操作类型,为对应的属性通过反射来赋值
if (operationType == OperationType.INSERT){
//为四个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if (operationType == OperationType.UPDATE){
//为两个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Redis
1)什么是Redis?
Redis是一个基于内存的 key-value 结构数据库。作为数据库,与 MySQL 类似,都是用来存储数据的。但不同的是,MySQL 是将数据以数据文件的方式存在磁盘上,本质上是磁盘存储,而 Redis 则是将数据存储在内存中的,本质上是内存存储。除了存储介质不一样,二者存储数据的结构也是不一样的,MySQL 是通过二维表来存储数据,而 Redis 则是基于 key-value 结构的,也就是键值对。如下图所示:
由于 Redis 是基于内存存储的,所以读写性能相对于 MySQL 会更高,但也正是因为其基于内存存储,而内存是有限的,所以不可能将所有的数据都存储到 Redis 当中。Redis 一般适合存储一些热点数据,热点数据的特点就是在某一个特定的时间点,会有大量的用户去访问它,比如在抢购秒杀的时候,会有大量的用户在这个时间点上同时去访问数据,这个时候数据就适合存储在 Redis 当中。由此可以看出,Redis 并不可以取代 MySQL,而相当于是对 MySQL 的一个补充,大部分情况下,二者是同时出现在一个项目中使用的,Redis 用来存储热点数据来提高读取性能,而 MySQL 则用来存储绝大部分的业务数据。
2)什么是 key-value 结构?
key是字符串类型,value有5种常用的数据类型:
字符串 string:普通字符串,Redis 中最简单的数据类型
哈希 hash:也叫散列,类似于Java中的 HashMap 结构
列表 list:按照插入顺序排序,可以有重复元素,类似于Java中的 LinkedList
集合 set:无序集合,没有重复元素,类似于 Java 中的 HashSet
有序集合 sorted set / zset:集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
各种数据类型的特点:
3)如何使用Redis?
使用Redis的Java客户端,常用的有:Jedis、Lettuce、Spring Data Redis,其中Spring Data Redis是Spring的一部分,对Redis底层开发进行了高度封装。在Spring项目中,可以使用Spring Data Redis来简化操作。
4)如何使用Spring Data Redis?
1.导入Spring Data Redis的maven坐标
2.配置Redis数据源
3.编写配置类,创建RedisTemplate对象
4.通过RedisTemplate对象操作Redis
5)MySQL压力过大
用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,比如同时间有很多顾客使用小程序,数据库的访问压力就会随之增大。
数据库的压力过大,会造成数据库的查询性能下降。给到用户的直观感受就是用户选择到了一个分类,需要过几秒钟,对应的分类信息才会显示出来,用户的使用体验就会非常差。那么如何来解决这个问题呢?
6)使用Redis缓存
前面我们说过,MySQL 本质上是磁盘存储,而 Redis 则是是内存存储,内存的操作相对于磁盘的操作来说,性能会高很多,所以可以通过 Redis 来缓存菜品数据,减少数据库的查询操作。流程如下:
本项目中缓存菜品的思路是什么呢?根据前端用户的使用需求来看,用户选择了一个分类,然后对应的分类信息便会展现出来给用户看,所以我们就是要根据分类来进行缓存,即每个分类下的菜品保存一份缓存数据。刚才也提到了 Redis 是一个 key-value 结构数据库,所以可以使用分类的id来作为我们缓存的 key,而 value 呢对应的就是每个分类下的菜品数据,相应的菜品数据采用 spring 字符串来保存。
此外,还需要注意一个点,当数据库中菜品数据有变更时需要清理缓存数据。比如管理端修改了菜品的价格,如果不清理掉缓存数据的话,用户端看到的还会是我们一开始缓存好的数据,就是修改前的价格,这样就不行咯。因此我们就需要在菜品数据有变更时清理缓存数据。介绍完了使用 Redis 缓存菜品,接下来,引入一个缓存框架,Spring Cache。
Spring Cache
1)什么是Spring Cache?
Spring Cache 是由 spring 提供的一个缓存框架,使用它可以进一步的简化我们的代码,通过注解的方式实现缓存的功能。当我们想要缓存数据时,只需要在相应的方法上加入它提供的注解即可。这种方式类似于事务管理,在要进行事务控制的时候,只需要在 Service 的方法上加入事务注解就可以了。二者是有点儿相似的。Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,如:EHCache、Caffeine、Redis,在本项目中,产品的数据都会缓存到 Redis 当中。但如果在后期,想换一个缓存实现,比如不想使用 Redis 了,想换成 EHCache 只需要再额外导入 EHCache 相关的 jar 包就可以了,而 Spring Cache 提供的注解,都是通用的。这样就可以非常轻松的来切换不同的缓存实现,且代码不需要再进行额外的修改。
2)如何使用Spring Cache?
1.导入 Spring Cache 的 maven 坐标
2.Spring Cache 常用注解
我们使用 Spring Cache 主要是通过注解的方式,以下是 Spring Cache 的常用注解及其作用。
Spring Task
Spring Task 是由 spring 框架提供的一个定时任务工具,可以按照约定的时间自动执行某个代码逻辑。在本项目中,考虑到实际使用情况会存在订单状态需要定时处理的时候,比如有超时的订单,还有一直处于派送中未完成的订单,这两类订单就需要通过定时任务来定时处理。那么我们如何使用 Spring Task 来指定定时任务的时间呢?
1)如何使用 Spring Task ?
1.导入 maven 坐标 spring-context
2.启动类添加注解 @EnableScheduling 开启任务调度
3.自定义任务类
package com.sky.task;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
/**
* 定时任务类
*/
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 处理超时订单的方法
*/
@Scheduled(cron = "0 * * * * ? ")//每分钟触发一次
public void processTimeoutOrder(){
log.info("处理超时订单: {}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
//查询超时订单select * from orders where status = ? and order_time < ?
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
if (ordersList != null && ordersList.size() > 0){
for (Orders orders : ordersList){
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("订单超时,自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
}
/**
* 处理一直处于派送中状态的订单
*/
@Scheduled(cron = "0 0 1 * * ?")//每天凌晨一点触发一次
public void processDeliveryOrder(){
log.info("定时处理处于派送中的订单: {}",LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
if (ordersList != null && ordersList.size() > 0){
for (Orders orders : ordersList){
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
}
}
}
}
上面代码 “@Scheduled(cron = “0 0 1 * * ?”)” 中的 0 0 1 * * ? 被称为 cron 表达式,什么是 cron 表达式呢?
2)Cron 表达式
cron 表达式其实就是一个字符串,通过 cron 表达式可以定义任务触发的时间。
构成规则:分为 6 或 7 个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
例如:2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ?2022
WebSocket
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。它与我们熟知的 http 协议是不一样的。
1)HTTP 协议和 WebSocket 协议对比:
1.HTTP 是短连接
2.WebSocket 是长连接
3.HTTP 通信是单向的,基于请求响应模式
4.WebSocket 支持双向通信
5.HTTP 和 WebSocket 底层都是TCP连接
如此比较并不是说 WebSocket 就没有缺点
2)WebSocket 的缺点
•服务器长期维护长连接需要一定的成本
•各个浏览器支持程度不一
•WebSocket 是长连接,受网络限制比较大,需要处理好重连
3)为什么要使用 WebSocket 协议?
本项目中,由于存在下单提醒和催单功能,因此需要使用到 WebSocket 实现管理端页面和服务端保持长连接状态。实现逻辑为当客户支付后,调用 WebSocket 的相关API实现服务端向客户端推送消息。客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报。约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content。其中 type 为消息类型,1为来单提醒,2为客户催单,orderId 为订单,idcontent 为消息内容。具体的催单业务代码如下:
user/OrderController:
/**
* 客户催单
* @param id
* @return
*/
@GetMapping("/reminder/{id}")
@ApiOperation("客户催单")
public Result reminder(@PathVariable("id") Long id){
orderService.reminder(id);
return Result.success();
}
OrderService:
/**
* 客户催单
* @param id
*/
void reminder(Long id);
OrderServiceImpl:
/**
* 客户催单
* @param id
*/
public void reminder(Long id) {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);
// 校验订单是否存在
if (ordersDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Map map = new HashMap();
map.put("type",2);
map.put("orderId",id);
map.put("content","订单号:" + ordersDB.getNumber());
//通过websocket向客户端浏览器推送消息
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}
Apache POI
Apache POI 是一个处理 Miscrosoft Office 各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对 Miscrosoft Office 各种文件进行读写操作。在本项目中,可以使用该技术将店家运营数据进行导出为Excel报表。具体实现代码如下:
ReportController:
/**
* 导出运营数据报表
* @param response
*/
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
reportService.exportBusinessData(response);
}
ReportService:
/**
* 导出运营数据报表
* @param response
*/
void exportBusinessData(HttpServletResponse response);
ReportServiceImpl:
/**
* 导出运营数据报表
* @param response
*/
public void exportBusinessData(HttpServletResponse response) {
//1. 查询数据库,获取营业数据---查询最近30天的运营数据
LocalDate dateBegin = LocalDate.now().minusDays(30);
LocalDate dateEnd = LocalDate.now().minusDays(1);
//查询概览数据
BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));
//2. 通过POI将数据写入到Excel文件中
InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
try {
//基于模板文件创建一个新的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
//获取表格文件的Sheet页
XSSFSheet sheet = excel.getSheet("Sheet1");
//填充数据--时间
sheet.getRow(1).getCell(1).setCellValue("时间:" + dateBegin + "至" + dateEnd);
//获得第4行
XSSFRow row = sheet.getRow(3);
row.getCell(2).setCellValue(businessDataVO.getTurnover());
row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
row.getCell(6).setCellValue(businessDataVO.getNewUsers());
//获得第5行
row = sheet.getRow(4);
row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
row.getCell(4).setCellValue(businessDataVO.getUnitPrice());
//填充明细数据
for (int i = 0; i < 30; i++) {
LocalDate date = dateBegin.plusDays(i);
//查询某一天的营业数据
BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
//获得某一行
row = sheet.getRow(7 + i);
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(3).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(5).setCellValue(businessData.getUnitPrice());
row.getCell(6).setCellValue(businessData.getNewUsers());
}
//3. 通过输出流将Excel文件下载到客户端浏览器
ServletOutputStream out = response.getOutputStream();
excel.write(out);
//关闭资源
out.close();
excel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
总结
十分高兴你可以看到这里,诚心希望这篇文章能对你的学习有所帮助。秉持着做一件事就要做好的原理,而且自己是一个什么都不懂的初学者,所以写完该项目后进行了这样的一个技术总结。文章中如有出错,欢迎大家指出错误,我愿意虚心求教并改正错误!让我们端正心态!一起继续加油吧!后续写的项目也会继续发布总结!
V:CysEa_