前言 博主写苍穹外卖本来是打算熟悉一下java环境和基本框架,为后续写其他项目积累一点经验,本来没有留下什么关于技术栈的总结,偶然看到这篇博客https://blog.csdn.net/m0_73612206/article/details/141526550
发现因为没有及时总结,只凭印象对各种技术栈其实并没有多么熟悉。借助ai对项目进行了分析,在大佬的博客里增加了很多关于这个项目的更具体的各种技术栈的使用,作为自用项目总结。
技术选型 技术选型:项目中使用到的技术框架和中间件等
1)什么是技术框架?
Java 技术框架是指在 Java 开发中广泛使用的一系列库、工具和结构化代码模式,这些框架为开发者提供了解决常见问题的通用方案。框架通常包括已经实现的模块、组件和工具集,可以减少开发时间、提高代码质量,并确保项目的一致性和可维护性。
2)什么是中间件?
Java 中间件是位于操作系统和应用程序之间的独立软件层,它为分布式应用程序提供通信、数据管理、事务处理、安全性、消息传递等服务。中间件通常用于简化应用程序之间的交互、数据传输和系统集成,常见的 Java 中间件包括应用服务器、消息队列、缓存系统、服务总线等。
下图为项目所涉及的技术选型:
下图为项目整体结构:
前端-管理端 Nginx Nginx 是一个高性能的 Web 服务器和反向代理服务器,常用于处理静态资源、负载均衡和转发客户端请求到后端服务。
1 正向代理 代理客户端,让客户端“借别人的身份访问服务器”。常见场景是科学上网。
2 反向代理 代理服务器,让服务器“借代理的身份接收客户端请求”
前端发送的请求地址为:http://localhost/api/employee/login,
而我们后端的接口地址则为:http://localhost:8080/admin/employee/login ,
可以很明显地看到前端请求地址和后端接口地址并不一样,但是却能请求成功,这就是因为通过了 Nginx 的反向代理,浏览器也就是我们用户,发出的请求先是请求到 Nginx 服务器,由 Nginx 反向代理把这个请求转发给后端的 tomcat 服务器。可能有人要问了,“那这不纯纯多此一举嘛,我直接发给 tomcat 不行嘛,为啥非要中间多这么个步骤?” 这就要说到 Nginx 反向代理的好处了。
常见用途:
2.1 负载均衡 负载均衡可以把大量的请求按着我们自己指定的方式均衡地分配给每台服务器,如下图所示,通过 Nginx 负载均衡,可以将前端发送的请求均匀地分发给服务A、B、C。如果没有 Nginx 负载均衡,就需要前端自己去访问后端的服务器,而前端只能固定的访问某一台服务器,效率就会大大折扣了。
2.2 隐藏真实服务器 上面说到如果没有 Nginx 负载均衡,就需要前端自己去访问后端的服务器,但实际在我们实际企业项目的开发中,类似于上图的服务A、B、C是不会直接暴露出来的,因此,由前端是无法直接请求到后端服务的,只能通过 Nginx 这个入口。
2.3 动静分离 传统方式处理接口请求、返回图片、返回CSS等搜友请求都经过后端服务器,压力过大,动静分离就是把网站中的静态资源和动态请求分开处理,提高系统性能。
2.4 提高访问速度
在请求到 Nginx 的时候,Nginx 会做一个缓存,如果我们请求一个之前请求过的接口地址,Nginx 就无须再请求真正的后端服务,而是会直接将缓存的数据响应给前端,访问速度会进一步的提高。
既然 Nginx 有那么多的优点,那我们该怎么去配置 Nginx 反向代理和 Nginx 负载均衡呢
3 配置反向代理
还是刚才登录的接口,由于请求路径中含有 “/api/” 当请求发到 Nginx 之后,Nginx 就会通过反向代理转发给后端 tomcat,而转发地址就是我们指定的地址,而后面那段动态的请求路径,也会追加到我们指定的地址后面,从而拼成一个完成的请求路径,而这个路径就正好对应了我们后端的接口地址,因此可以请求成功。
4 配置负载均衡
因为本地只有一台服务器,所以下面的服务器地址被注释掉了,weight是权重,weight值越高,被转发到该服务器的请求越多。这种Nginx负载均衡策略是按照权重来分,还有一下策略:
Apache ECharts Apache ECharts 是一款基于 Javascript 的数据可视化图库表,提供直观、生动、可交互、可个性化定制的数据可视化图表。使用 Echarts,重点在于研究当前图表所需的数据格式 。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表
1 Apache ECharts的导入 官方文档:https://echarts.apache.org/handbook/zh/get-started/
在 https://www.jsdelivr.com/package/npm/echarts 获取dist/echarts.js
前端-用户端 微信小程序 一种新的开放能力,可以在微信内被便捷地获取和传播,同时具有出色的使用体验。那么开发微信小程序之前需要做哪些准备呢?
开发微信小程序所要做的准备
1.注册小程序
2.完善小程序信息
3.下载开发者工具
本项目微信小程序端的注册登录和微信支付功能都需要阅读微信提供的接口文档,向为辐接口服务发送数据并获取数据,下图为小程序登录的接口文档。
后端 Spring MVC pring MVC 是 Spring 框架中专门用于构建 Web 应用的模块,它实现了经典的 MVC(Model-View-Controller) 设计模式,帮助开发者快速、规范地开发基于 HTTP 的 Web 应用。
1 Spring MVC 的核心工作流程 1 2 3 4 5 6 7 客户端请求 → DispatcherServlet → HandlerMapping → Controller → Service → Mapper → 数据库 ↓ ↓ ↓ (前端控制器) (路由映射) (业务处理) 客户端响应 ← ViewResolver ← 返回结果 ← Controller ← Service ← Mapper ← 数据库结果 ↓ ↓ (视图解析) (封装响应)
2 本项目中 Spring MVC 的实际应用 3 本项目中 Spring MVC 的实际应用 3.1 Controller 层 以 OrderController.java 为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController("userOrderController") @RequestMapping("/user/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/submit") public Result<OrderSubmitVO> submit (@RequestBody OrdersSubmitDTO ordersSubmitDTO) { OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO); return Result.success(orderSubmitVO); } }
Spring MVC 在这里做了什么?
@RestController :告诉 Spring 这是一个处理 HTTP 请求的类
@RequestMapping(“/user/order”) :设置基础路径,所有方法的 URL 都以此为前缀
@PostMapping(“/submit”) :映射 POST 请求的路径为 /user/order/submit
@RequestBody :自动将 JSON 请求体反序列化为 Java 对象
Result :自动将返回的对象序列化为 JSON 响应
3.2 请求处理流程 当 C 端用户在小程序点击”下单”时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ① 小程序发送 POST 请求到 /user/order/submit(JSON 格式的地址、购物车等数据) ↓ ② DispatcherServlet 接收请求,查找匹配的 Controller 方法 ↓ ③ 找到 OrderController.submit() 方法 ↓ ④ Spring MVC 自动完成: - JWT 拦截器验证 token(WebMvcConfiguration 中配置的) - JSON 参数 → OrdersSubmitDTO 对象(@RequestBody ) - 调用 orderService.submitOrder() 处理业务 ↓ ⑤ Service 层查询地址簿、检查配送范围、创建订单 ↓ ⑥ Controller 拿到结果,Spring MVC 自动封装为 JSON 响应 ↓ ⑦ 小程序收到下单结果
3.3 参数绑定 Spring MVC 能自动将 HTTP 请求参数绑定到 Java 方法参数上:
1 2 3 4 5 6 7 8 9 10 11 @GetMapping ("/{id}" )public Result <Orders > getById (@PathVariable Long id ) { ... }@GetMapping ("/list" )public Result <PageResult > list (int page, int pageSize ) { ... }@PostMapping ("/submit" )public Result <OrderSubmitVO > submit (@RequestBody OrdersSubmitDTO dto ) { ... }
3.4 返回值处理 使用 @RestController 时,Spring MVC 自动将返回值序列化为 JSON:
1 2 3 4 public Result<OrderSubmitVO> submit (...) { OrderSubmitVO vo = orderService.submitOrder(dto); return Result.success(vo); }
Spring MVC 会将 Result.success(vo) 转换为:
1 2 3 4 5 6 7 8 9 10 { "code" : 1 , "msg" : null , "data" : { "id" : 123456789 , "orderTime" : "2026-08-04T10:30:00" , "orderNumber" : "1722681000000" , "orderAmount" : 45.00 } }
三层架构 1 什么是三层架构 后端对操作的处理逻辑上看可以分为三个部分,
数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。
逻辑处理:负责业务逻辑处理的代码。
请求处理、响应数据:负责,接收页面的请求,给页面响应数据。
按照上述的三个组成部分,在项目开发中可以将代码分为三层,如图所示:
Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
Service:业务逻辑层。处理具体的业务逻辑。
Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。
前端发起的请求,由Controller层接收(Controller响应数据给前端)
Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)
2 什么是耦合 耦合 指的是两个模块之间依赖程度太高,一个模块发生变动,另一个模块也要跟着改。
耦合 指的是两个模块之间依赖程度太高,一个模块发生变动,另一个模块也要跟着改。
用反例说明:高耦合的写法
假设 OrderController.java 是这样写的:
1 2 3 4 public class OrderController { private OrderService orderService = new OrderServiceImpl (); }
问题在哪?
Controller 必须知道 OrderServiceImpl 的存在
如果要换一个新的 OrderServiceImpl2 ,Controller 代码必须改
Controller 和具体实现类绑死了 → 这就是高耦合
3 什么是解耦 解耦 就是让模块之间通过 接口/抽象 来协作,而不是直接依赖具体实现。
本项目的正确写法是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController("userOrderController") @RequestMapping("/user/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/submit") public Result<OrderSubmitVO> submit (@RequestBody OrdersSubmitDTO ordersSubmitDTO) { OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO); return Result.success(orderSubmitVO); } }
OrderService 是接口, OrderServiceImpl 是实现类。
Controller 只声明了 OrderService orderService ,它根本不知道具体是 OrderServiceImpl 还是其他实现类。以后要换实现类,Controller 一行代码都不用改 。
这就是解耦: 面向接口编程,依赖抽象而不是具体实现。
IOC&DI 1 什么是IOC IOC = Inversion of Control ,控制反转。
意思是: 对象的创建权、管理权从程序员手中”反转”给了 Spring 容器。
2 本项目中的 IOC 看 OrderServiceImpl.java :
1 2 3 4 5 6 7 8 9 10 11 12 13 @Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderDetailMapper orderDetailMapper; @Autowired private ShoppingCartMapper shoppingCartMapper; @Autowired private AddressBookMapper addressBookMapper; @Autowired private WeChatPayUtil weChatPayUtil;
这些对象都不是程序员 new 出来的,而是 Spring 在启动时扫描到 @Service 注解后,自动创建对象放进容器里,运行时按需分配。
Spring 容器就像一个大工厂,启动时把所有标了 @Service 、 @Controller 、 @Mapper 等注解的类都实例化好,放进”容器”中。程序运行时,你需要什么,它就从容器里拿什么给你。
3 为什么使用IOC 使用 IOC 是为了把”对象的创建和管理”从业务代码中抽离出来,交给 Spring 容器统一负责,从而降低模块之间的耦合,让代码更容易维护、扩展和测试。
当项目变大、依赖变多、团队协作时,手动创建和管理对象会导致代码高度耦合、难以维护和测试。 把对象的创建和管理交给 Spring,程序员专注于业务逻辑 ,是现代 Java 后端开发的标准做法。
4 什么是DI DI = Dependency Injection ,依赖注入。
它是 IOC 的一种具体实现方式。意思是: Spring 容器把对象所依赖的其他对象,自动”注入”进去。
本项目中的 DI 表现形式
字段注入(Field Injection)
1 2 @Autowired private OrderService orderService;
这是本项目最常用的方式。Spring 会自动在容器中找到 OrderService 的实现类( OrderServiceImpl ),注入到这个字段中。
MyBatis 什么是MyBatis MyBatis 是一个 Java 持久层框架(ORM 框架),它的核心作用是: 在 Java 对象和数据库 SQL 之间建立映射,让程序员能通过调用 Java 方法来操作数据库,而不用写一堆 JDBC 样板代码。
为什么要使用MyBatis 假设没有 MyBatis,在 OrderServiceImpl 里查询一条地址记录,要这样写:
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 public AddressBook getById (Long id) { Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; try { conn = DriverManager.getConnection(url, user, password); ps = conn.prepareStatement("select * from address_book where id = ?" ); ps.setLong(1 , id); rs = ps.executeQuery(); if (rs.next()) { AddressBook ab = new AddressBook (); ab.setId(rs.getLong("id" )); ab.setUserId(rs.getLong("user_id" )); ab.setConsignee(rs.getString("consignee" )); ab.setPhone(rs.getString("phone" )); ab.setProvinceName(rs.getString("province_name" )); return ab; } } catch (SQLException e) { e.printStackTrace(); } finally { if (rs != null ) try { rs.close(); } catch (SQLException e) {} if (ps != null ) try { ps.close(); } catch (SQLException e) {} if (conn != null ) try { conn.close(); } catch (SQLException e) {} } return null ; }
用了 MyBatis 之后则只需要定义一个接口,
MyBatis 会自动完成:
打开数据库连接
把 #{id} 替换为参数值
执行 SQL
把查询结果自动映射为 AddressBook 对象
关闭连接
1 2 3 4 5 @Mapper public interface AddressBookMapper { @Select("select * from address_book where id = #{id}") AddressBook getById (Long id) ; }
PageHelper 1 什么是PageHelper PageHelper 是 MyBatis 的分页插件,核心价值是你写的 SQL 不需要 LIMIT,它自动帮你加 。应用场景是分页查询。PageHelper 是 MyBatis 的一个 分页插件 。它的核心原理是: 在执行 SQL 之前拦截并自动拼接 LIMIT ? OFFSET ? ,让原来不分页的 SQL 变成分页 SQL 。
2 怎么使用PageHelper 2.1 配置依赖 父pom.xml 中定义版本
1 2 3 4 5 6 7 8 9 10 11 12 13 <properties > <pagehelper > 1.3.0</pagehelper > </properties > <dependencyManagement > <dependencies > <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > <version > ${pagehelper}</version > </dependency > </dependencies > </dependencyManagement >
sky-server 模块中引入
1 2 3 4 <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> </dependency>
2.2 通用三步模式使用 项目中所有分页查询都遵循同样的三步:
1 2 3 4 5 6 7 8 9 // ① 启动分页 —— 设置第几页、每页多少条 PageHelper.startPage(pageNum, pageSize); // ② 执行查询 —— 注意:SQL 里没有 LIMIT Page<Xxx> page = xxxMapper.pageQuery(queryDTO); // ③ 拿到分页结果 long total = page.getTotal(); // 总记录数 List<Xxx> records = page.getResult(); // 当前页数据
MyBatis Dynamic SQL 1 什么是 Dynamic SQL MyBatis Dynamic SQL (MyBatis 动态 SQL),是 MyBatis 框架内置的一项核心能力。它允许在 XML 映射文件中用标签来编写 可以根据条件变化的 SQL 语句 。
2 Dynamic SQL的动态标签 2.1 — 条件判断 语法 :
本项目实例 : OrderMapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <select id ="pageQuery" resultType ="Orders" > select * from orders <where > <if test ="number != null and number!=''" > and number like concat('%',#{number},'%') </if > <if test ="phone != null and phone!=''" > and phone like concat('%',#{phone},'%') </if > <if test ="userId != null" > and user_id = #{userId} </if > <if test ="status != null" > and status = #{status} </if > <if test ="beginTime != null" > and order_time > = #{beginTime} </if > <if test ="endTime != null" > and order_time < = #{endTime} </if > </where > order by order_time desc </select >
执行逻辑 :根据传入的 OrdersPageQueryDTO 中哪些字段不为 null,动态拼接对应的 WHERE 条件 。全部为 null 时,SQL 变成 select * from orders order by order_time desc (无条件查询)。
2.2 — 智能 WHERE 子句 语法 :
1 2 3 4 <where > <if test ="..." > and xxx</if > <if test ="..." > and yyy</if > </where >
作用 : 有两个智能行为:
如果内部有至少一个条件成立,自动加 WHERE 关键字
自动去掉 第一个条件前面的 AND 或 OR
2.3 — 智能 SET 子句 语法 :
1 2 3 4 <set > <if test ="..." > 字段名 = #{值},</if > <if test ="..." > 字段名 = #{值},</if > </set >
作用 :自动去掉 最后一个字段后面的逗号 ,防止 SQL 语法错误。
2.4 — 循环遍历 1 2 3 <foreach collection ="集合名" item ="元素变量" separator ="," open ="(" close =")" > #{元素变量} </foreach >
本项目实例:批量删除
1 2 3 4 5 6 7 <delete id ="deleteByIds" > delete from dish where id in <foreach collection ="ids" item ="id" open ="(" separator ="," close =")" > #{id} </foreach > </delete >
渲染结果(传入 ids = [1, 2, 3] ):
1 DELETE FROM dish WHERE id IN (1 , 2 , 3 )
HttpClient 在本项目(苍穹外卖)中,HttpClient 的唯一用途是: 调用百度地图 API,检测用户的收货地址是否在商家的配送范围内。
具体业务场景:用户下单时,系统需要验证用户填写的地址是否超过了商家设定的配送距离(比如 5 公里以内)。这就需要通过地图服务来获取商家和用户地址的经纬度,并计算两点之间的实际距离。
1 什么是HttpClient HttpClient 是 Java 中用于 发送 HTTP 请求 的客户端工具库,它模拟了浏览器的行为,允许后端服务像访问网页一样去调用外部的接口(API)、下载资源等。
简单来说,它就是 Java 版本的”浏览器请求工具”,常用于:
调用第三方服务的 API(如支付、地图、短信服务)
爬取网页数据
微服务之间的互相调用
2 如何使用HttpClient 2.1 导入maven坐标
本项目中因为已经导入了 aliyun-sdk-oss,而其底层使用到了 HttpClient,就不需要我们再去导坐标了
2.2 创建HttpClient对象
项目没有直接使用原始的 Apache HttpClient,而是封装成了一个静态工具类 HttpClientUtil.java ,底层基于 Apache HttpClient 4.x 。
它提供了三个核心方法:
工具类还做了这些处理:
设置了 5秒超时 ( TIMEOUT_MSEC ),避免外部接口卡死导致系统阻塞
自动把 Map<String,String> 参数拼成 URL query string 或表单
自动处理响应编码(UTF-8)
自动关闭资源(防止连接泄露)
2.3 创建Http请求对象
2.4 调用HttpClient的execute方法发送请求
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 45 46 public static String doGet (String url,Map <String ,String > paramMap ){ CloseableHttpClient httpClient = HttpClients .createDefault (); String result = "" ; CloseableHttpResponse response = null ; try { URIBuilder builder = new URIBuilder (url); if (paramMap != null ){ for (String key : paramMap.keySet ()) { builder.addParameter (key,paramMap.get (key)); } } URI uri = builder.build (); HttpGet httpGet = new HttpGet (uri); response = httpClient.execute (httpGet); if (response.getStatusLine ().getStatusCode () == 200 ){ result = EntityUtils .toString (response.getEntity (),"UTF-8" ); } }catch (Exception e){ e.printStackTrace (); }finally { try { response.close (); httpClient.close (); } catch (IOException e) { e.printStackTrace (); } } return result; }
2.5 实际业务调用
创建好HttpClient工具类后,后续业务可以直接调用
1 2 3 4 5 6 7 8 9 10 11 12 private void checkOutOfRange (String address ) { Map map = new HashMap (); map.put ("address" , shopAddress); map.put ("output" , "json" ); map.put ("ak" , ak); String shopCoordinate = HttpClientUtil .doGet ( "https://api.map.baidu.com/geocoding/v3" , map); }
JWT 1 JWT的概念 JWT 全称:JSON Web Token,定义了一种简洁的、自包含的格式,用于在通信双方以 JSON 数据格式安全的传输信息。
2 JWT令牌的组成 整个 JWT 令牌是由三个部分组成的,三个部分之间只用 “.” 来分隔
第一部分
是头部区域,其记录的内容比较固定,是令牌的类型以及签名算法,它的数据格式就是JSON数据格式。
上面大括号中是原始的 JSON 数据格式,alg 指定的就是签名算法,这里指定的算法就是 HS256,将来就会根据这里指定的算法对令牌进行数字签名,type 指定的就是令牌的类型,这里令牌的类型是 JWT。那么是如何由上面原始的 JSON 数据格式变成下面这样的字符串的呢?在生成 JWT 令牌的时候,会对上面原始的 JSON 数据格式的字符串进行一次 Base64编码。
第二部分
第二部分是有效载荷,可以携带一些自定义信息和一些默认信息。
与头部区域一样,原始数据依旧是 JSON 数据格式的字符串,进行 Base64编码后变成下面这样的字符串。
第三部分
第三部分是签名,签名的目的是防止 Token 被篡改,从而确保令牌的安全性,而这个签名呢,正是通过第一部分头部区域所指定的签名算法所算出来的。并不是 Base64编码,而且还会融入前面第一部分和第二部分的内容。
现在能理解为什么 JWT 是用于在通信双方以 JSON 数据格式安全的传输信息的了吧,因为原始的数据格式就是 JSON 数据格式的。
3 如何生成JWT令牌? 1.导入maven坐标
2.生成JWT令牌
3.校验JWT令牌
4.注意事项
Swagger 1 Swagger 能做什么? Swagger 是一个用于 生成和管理 RESTful API 文档 的开源工具。它的核心理念是”代码即文档”——通过在代码中添加注解,自动生成在线的、可交互的 API 文档页面。
自动生成接口文档 :扫描 Controller 类,自动生成接口列表
在线测试接口 :提供 Web UI,可以直接在浏览器里测试 API(类似 Postman)
参数说明 :标注每个接口的参数类型、是否必填、返回值结构
团队协作 :前后端开发者可以基于统一的文档进行协作开发
2 什么是Knife4j 本项目使用的是 Knife4j 3.0.2 ,它是 Swagger 2 的增强 UI 工具,提供了更美观、更强大的接口文档界面。
原因 :
双端业务 :苍穹外卖同时有 C端(微信小程序)和 B端(管理后台),两套接口需要清晰的文档
团队协作 :前端开发人员可以直接通过文档页面了解后端接口,无需反复沟通
快速调试 :开发过程中可以直接在文档页面测试接口,不需要额外的 Postman
3 怎么使用Knife4j 3.1 配置依赖
(1)在父 pom.xml 中统一管理版本:
1 <knife4j>3.0.2</knife4j>
(2)在 sky-server 模块中引入依赖:
3.2 在配置类中加入knife4j相关配置
关键设计 :项目创建了 两个 Docket ,分别对应 C端和 B端接口,这样在文档页面可以通过下拉框切换查看不同客户端的接口。
3.3 设置静态资源映射
这一步是为了让 Knife4j 的前端页面能正常访问(访问地址: http://localhost:8080/doc.html )。
3.4 Swagger常用注解
3.5 访问Swagger文档
启动项目后,访问: http://localhost:端口/doc.html
文档页面会显示两个分组:
管理端接口 : /admin/** 路径下的所有接口
用户端接口 : /user/** 路径下的所有接口
每个接口都会展示:
请求方法(GET/POST/PUT/DELETE)
请求路径
参数列表(类型、是否必填)
返回值结构
可点击”调试”按钮直接测试接口
过滤器Filtter 1 什么是过滤器Filter? 过滤器Filter,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一,过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能,过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理等。
2 如何使用过滤器Filter? 1.定义Filter
定义一个类,实现 Filter接口,并重写其所有方法
2.配置Filter
Filter类上加 @WebFilter 注解,配置拦截资源的路径。
在 Filter类上加 @WebFilter 注解后,还需要在启动类上也加上一个注解,@ServletComponentScan,因为 Filter 是 JavaWeb 三大组件之一,并不是 spring boot 当中提供的,而如果想要在 spring boot 项目中使用 JavaWeb 三大组件就必须要在启动类上加上@ServletComponentScan 注解,加上该注解后就表示当前项目是支持 Servlet 相关组件的。
3 过滤器Filter执行流程
4 过滤器Filter拦截路径 过滤器Filter 可以根据需求,配置不同的拦截资源路径,上面我们配置过滤器Filter 时,拦截的是 ”/*“ ,也可以自己配置自己所需的。常见的配置方式有如下三种:
拦截器Interceptor 1 Fillter和Interceptor的区别 Filtter所属框架是Java Servlet 规范下的,而Interceptor是Spring MVC框架下的。两者功能相似但是机制不同,对比如下:
在本项目中,使用Interceptor用于拦截用户端、管理端的大多数请求,校验登录状态,防止权限绕过。
本项目用 Interceptor 已足够,但果需要在请求到达 Spring 之前就处理,或者拦截静态资源就需要使用过滤器Fillter。
2 如何使用拦截器Interceptor 1 定义Interceptor 定义一个类,实现 Interceptor接口,并重写其所有方法
以 JwtTokenUserInterceptor.java 为例:
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 @Component @Slf4j public class JwtTokenUserInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true ; } String token = request.getHeader(jwtProperties.getUserTokenName()); try { Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString()); BaseContext.setCurrentId(userId); return true ; } catch (Exception ex) { response.setStatus(401 ); return false ; } } }
2 注册拦截器 1 2 3 4 5 6 7 8 9 10 11 12 13 @Override protected void addInterceptors (InterceptorRegistry registry ) { registry.addInterceptor (jwtTokenAdminInterceptor) .addPathPatterns ("/admin/**" ) .excludePathPatterns ("/admin/employee/login" ); registry.addInterceptor (jwtTokenUserInterceptor) .addPathPatterns ("/user/**" ) .excludePathPatterns ("/user/user/login" ) .excludePathPatterns ("user/shop/status" ); }
AOP AOP 是一种”横切”编程思想,把分散在多个模块的通用逻辑(日志、事务、权限、公共字段填充)统一抽离到切面中。本项目菜品管理功能通过 AutoFillAspect 切面 + @AutoFill 自定义注解,在 Mapper 方法执行前自动填充 createTime 、 createUser 、 updateTime 、 updateUser 四个公共字段,消除了 Service 层的大量重复代码,实现了真正的 关注点分离 ——业务代码只管业务,通用的填充逻辑交给 AOP。
1 什么是AOP AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于在不改变原有代码的情况下,通过“切面”(Aspect)来增强或修改程序的行为。它通过将横切关注点(如日志记录、安全性、事务管理等)从核心业务逻辑中分离出来,减少代码的重复性,提高代码的可维护性。
2 AOP的核心概念
3 如何使用AOP 在本项目中,有许多业务表都有几个公共字段,比如员工表和分类表,这两张表中,都包含了这四个字段,如下图所示:
当我们向这些表中插入数据的时候,就需要为这些字段来赋值,如果每一个业务都要这么去赋值的话,代码就会冗余,而且不方便我们后期去维护,如何解决这些问题呢?首先需要明确一下这些字段分别是在什么时候去操作的,如下表所示:其中insert:插入操作,update:更新操作
整体的思路就是通过 AOP切面来统一处理,比如说要调用持久层的某个 Mapper,来进行一个 insert操作,这个时候就可以通过切面来统一拦截 Mapper这一层,来为我们的这几个公共字段进行赋值。这个时候又存在一个问题,这个持久层中并不是每个方法都需要去拦截然后赋值,比如进行一个查询方法,显然是不需要上面这几个公共字段的,这时候就需要有一种手段,能够知道当前这个持久层的操作需不需要来为这几个公共字段进行赋值。这种手段就是——定义注解 。为 Mapper 的方法来加入注解,加入注解就表示当前方法需要被拦截 ,需要为这几个公共字段进行赋值,如果没有加注解,就不需要处理。具体方法如下:
1 .自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
2 .自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill注解的方法,通过反射为公共字段赋值
3 .在 Mapper 的方法上加入 AutoFill注解
3.1 自定义注解
1 2 3 4 5 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { OperationType value () ; }
3.2 编写切面 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 @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) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); OperationType operationType = autoFill.value(); Object[] args = joinPoint.getArgs(); Object entity = args[0 ]; LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId(); if (operationType == OperationType.INSERT) { entity.getClass().getDeclaredMethod("setCreateTime" , LocalDateTime.class).invoke(entity, now); entity.getClass().getDeclaredMethod("setCreateUser" , Long.class).invoke(entity, currentId); entity.getClass().getDeclaredMethod("setUpdateTime" , LocalDateTime.class).invoke(entity, now); entity.getClass().getDeclaredMethod("setUpdateUser" , Long.class).invoke(entity, currentId); } else if (operationType == OperationType.UPDATE) { entity.getClass().getDeclaredMethod("setUpdateTime" , LocalDateTime.class).invoke(entity, now); entity.getClass().getDeclaredMethod("setUpdateUser" , Long.class).invoke(entity, currentId); } } }
3.3 在Mapper方法上打注解 1 2 3 4 5 6 @AutoFill(value = OperationType.INSERT) @Insert("insert into category(...)") void insert (Category category) ;@AutoFill(value = OperationType.UPDATE) void update (Category category) ;
Redis 1 什么是Redis Redis是一个基于内存 的 key-value 结构数据库 。
由于 Redis 是基于内存存储的,所以读写性能相对于 MySQL 会更高。Redis 一般适合存储一些热点数据,是对 MySQL 的一个补充,Redis 用来存储热点数据来提高读取性能,而 MySQL 则用来存储绝大部分的业务数据。
2 Redis操作对象 字符串 string:普通字符串,Redis 中最简单的数据类型
哈希 hash:也叫散列,类似于Java中的 HashMap 结构
列表 list:按照插入顺序排序,可以有重复元素,类似于Java中的 LinkedList
集合 set:无序集合,没有重复元素,类似于 Java 中的 HashSet
有序集合 sorted set / zset:集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
各种数据类型的特点:
3 怎么使用Redis 3.1 导入Spring Data Redis的maven坐标
3.2 配置Redis数据源 连接配置 1 2 3 4 5 6 7 8 9 10 11 12 spring: redis: host: ${sky.redis.host} port: ${sky.redis.port} password: ${sky.redis.password} database: ${sky.redis.database} sky: redis: host: localhost port: 6379 password: 123456 database: 10
3.3 编写配置类,创建RedisTemplate对象 1 2 3 4 5 6 7 8 9 10 11 12 @Configuration public class RedisConfiguration { @Bean public RedisTemplate redisTemplate (RedisConnectionFactory redisConnectionFactory) { RedisTemplate redisTemplate = new RedisTemplate (); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer ()); return redisTemplate; } }
3.4 通过RedisTemplate对象操作Redis 场景一:存储店铺营业状态(简单键值对) 业务需求 :B端管理员设置店铺状态(营业/打烊),C端用户查看店铺状态。这是一个简单值,不需要查数据库。
B端 - 设置 : admin/ShopController.java :
1 2 3 4 5 @PutMapping("/{status}") public Result setStatus (@PathVariable Integer status) { redisTemplate.opsForValue().set("SHOP_STATUS" , status); return Result.success(); }
C端 - 读取 : user/ShopController.java :
1 2 3 4 5 @GetMapping("/status") public Result<Integer> getStatus () { Integer status = (Integer) redisTemplate.opsForValue().get("SHOP_STATUS" ); return Result.success(status); }
场景二:缓存菜品列表(手动缓存模式) 业务需求 :C端用户按分类浏览菜品。菜品数据不常变,但访问频率高,缓存起来避免每次查数据库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @GetMapping("/list") public Result<List<DishVO>> list (Long categoryId) { String key = "dish_" + categoryId; List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); if (list != null && list.size() > 0 ) { return Result.success(list); } Dish dish = new Dish (); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE); list = dishService.listWithFlavor(dish); redisTemplate.opsForValue().set(key, list); return Result.success(list); }
4 注意事项 当对数据库中数据进行修改后没有在缓存中进行更新,会导致两边数据不一样,所以需要对缓存进行删除。
Spring cache 1 什么是Spring cache Spring Cache 是 Spring 框架提供的一套 缓存抽象层 。它不是具体的缓存实现,而是一套统一的 API 和注解,让你在 不关心底层用什么缓存技术 的情况下,用声明式注解完成缓存操作。
Spring cache相当于一种缓存服务调用规则,可以使用多种存储后端
2 为什么要使用Spring Cache 相比通过RedisTemplate对象操作Redis,每种缓存场景都要进行:查缓存->判断->查数据库->写缓存的过程,使用Spring Cache一个注解就可以自动完成:
第一次调用:执行方法 → 返回值自动存入 Redis
后续调用:直接从 Redis 取,方法体不执行
3 如何使用Spring Cache 3.1 开启缓存功能 1 2 3 4 5 6 7 @SpringBootApplication @EnableCaching public class SkyApplication { public static void main (String[] args) { SpringApplication.run(SkyApplication.class, args); } }
3.2 使用@Cacheable缓存查询结果 1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/list") @Cacheable(cacheNames = "setmealCache", key = "#categoryId") public Result<List<Setmeal>> list (Long categoryId) { Setmeal setmeal = new Setmeal (); setmeal.setCategoryId(categoryId); setmeal.setStatus(StatusConstant.ENABLE); List<Setmeal> list = setmealService.list(setmeal); return Result.success(list); }
3.3@CacheEvict清除缓存 1 2 3 4 5 6 @DeleteMapping @CacheEvict(cacheNames = "setmealCache", allEntries = true) public Result delete (@RequestParam List<Long> ids) { setmealService.deleteBatch(ids); return Result.success(); }
4 Spring Cache核心注解
5 注解属性 1 2 3 4 5 6 @Cacheable( cacheNames = "setmealCache", // 缓存命名空间,最终 Redis key 是 "setmealCache::10" key = "#categoryId", // SpEL 表达式,指定缓存的 key condition = "#categoryId > 0", // 条件:只有满足条件才缓存 unless = "#result == null" // 排除条件:结果为 null 时不缓存 )
Spring Task SpringTask是Spring框架下提供的定时任务工具,使用它可以定时处理一些任务,在本项目中商户订单状态定时处理使用了这一工具。
1 什么是Spring Task Spring Task是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定时任务的执行时间是由Cron表达式来确定的。
2 Cron表达式 cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒分钟、小时、日、月、周、年(可选)
可以使用Cron表达式在线生成器来生成
https://cron.ciding.cc/
3 怎么使用Spring Task ① 导入maven坐标 spring-context(已存在) ② 启动类添加注解@EnableScheduling 开启任务调度 ③ 自定义定时任务类
WebSocket WebSocket是一种协议,通过协议可以实现客户端和用户端数据进行双向传递,基于WebSocket可以向客户端推送消息,本项目中用户催单提醒和来单提醒使用了该协议。
1 什么是WebSocket WebSokcet是基于TCP的一种新的网络协议。他实现了浏览器与服务器全双工通信-浏览器与服务器 只需要完成一次握手,两者之间就可以创建持续性的连接并进行双向 数据传输。
特点:双向连接、长连接
缺点:
•服务器长期维护长连接需要一定的成本
•各个浏览器支持程度不一
•WebSocket 是长连接,受网络限制比较大,需要处理好重连
应用场景:视频弹幕、网页聊天、体育实况更新、股票基金报价实时更新。
2 本项目WebSocket具体使用 2.1 定义配置类,用于注册WebSocket的Bean
通过WebSocket实现管理端页面和服务端向客户端推送消息
客户端发送请求建立连接,使用的协议是ws协议。
2.1.1 maven坐标
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency >
2.1.2 使用WebSocket组件,用于和客户端通信
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package com.sky .websocket ; import org.springframework .stereotype .Component ;import javax.websocket .OnClose ;import javax.websocket .OnMessage ;import javax.websocket .OnOpen ;import javax.websocket .Session ;import javax.websocket .server .PathParam ;import javax.websocket .server .ServerEndpoint ;import java.util .Collection ;import java.util .HashMap ;import java.util .Map ;@Component @ServerEndpoint ("/ws/{sid}" ) public class WebSocketServer { private static Map <String , Session > sessionMap = new HashMap (); @OnOpen public void onOpen (Session session, @PathParam ("sid" ) String sid ) { System .out .println ("客户端:" + sid + "建立连接" ); sessionMap.put (sid, session); } @OnMessage public void onMessage (String message, @PathParam ("sid" ) String sid ) { System .out .println ("收到来自客户端:" + sid + "的信息:" + message); } @OnClose public void onClose (@PathParam ("sid" ) String sid ) { System .out .println ("连接断开:" + sid); sessionMap.remove (sid); } public void sendToAllClient (String message ) { Collection <Session > sessions = sessionMap.values (); for (Session session : sessions) { try { session.getBasicRemote ().sendText (message); } catch (Exception e) { e.printStackTrace (); } } } }
2.1.3 导入WebSocket配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.sky.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter (); } }
2.2 客户端浏览器解析服务端推送的消息
判断是来单提醒还是客户存单,进行相应的消息提示和语音播报
2.3 约定服务端发送给客户端浏览器的数据为JSON
字段包括type,orderId,content
type为订单类型,1为来单提醒 2为客户催单
orderId为订单id
content为消息内容
Apache POI 1 什么是Apache POI Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在Java 程
序中对Miscrosoft Office各种文件进行读写操作。
一般情况下,POI 都是用于操作 Excel 文件。
2 Apache POI的使用 配置依赖
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi</artifactId > <version > 3.16</version > </dependency > <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi-ooxml</artifactId > <version > 3.16</version > </dependency >
2.1 使用Apache POI创建并写入excel 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 public static void write () throws Exception{ XSSFWorkbook excel = new XSSFWorkbook (); XSSFSheet sheet = excel.createSheet("info" ); XSSFRow row = sheet.createRow(1 ); row.createCell(1 ).setCellValue("姓名" ); row.createCell(2 ).setCellValue("城市" ); row = sheet.createRow(2 ); row.createCell(1 ).setCellValue("张三" ); row.createCell(2 ).setCellValue("北京" ); row = sheet.createRow(3 ); row.createCell(1 ).setCellValue("李四" ); row.createCell(2 ).setCellValue("南京" ); FileOutputStream out = new FileOutputStream (new File ("D:\\info.xlsx" )); excel.write(out); excel.close(); out.close(); }
2.2 使用Apache POI 读出excel内容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static void read () throws Exception{ InputStream in = new FileInputStream (new File ("D:\\info.xlsx" )); XSSFWorkbook excel = new XSSFWorkbook (in); XSSFSheet sheet = excel.getSheetAt(0 ); int lastRowNum = sheet.getLastRowNum(); for (int i = 1 ; i <= lastRowNum ; i++) { XSSFRow row = sheet.getRow(i); String cellValue1 = row.getCell(1 ).getStringCellValue(); String cellValue2 = row.getCell(2 ).getStringCellValue(); System.out.println(cellValue1 + " " + cellValue2); } in.close(); excel.close(); }
2.3 本项目中使用POI实现运营数据下载 这个接口的功能是生成一个 Excel 文件,让浏览器下载。需要用HttpServletResponse获取后端要返回给浏览器的 HTTP 响应。
1 2 3 4 5 6 7 8 @GetMapping ("/export" )@ApiOperation ("导出运营数据表" )public void export (HttpServletResponse response ){ reportService.exportBusinessDate (response); }
具体功能实现在service层实现,根据数据模板要查询的数据包括概览数据和详细数据。
创建XSSFWorkbook时可以传入InputStream参数,作为创建的excel的模板,获取excel文件的sheet之后依次获取行、列进行填充。
填充完通过输出流将Excel文件下载到客户端浏览器,最后关闭资源。
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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public void exportBusinessData (HttpServletResponse response) { 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)); InputStream in = this .getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx" ); try { XSSFWorkbook excel = new XSSFWorkbook (in); XSSFSheet sheet = excel.getSheet("Sheet1" ); sheet.getRow(1 ).getCell(1 ).setCellValue("时间:" + dateBegin + "至" + dateEnd); XSSFRow row = sheet.getRow(3 ); row.getCell(2 ).setCellValue(businessDataVO.getTurnover()); row.getCell(4 ).setCellValue(businessDataVO.getOrderCompletionRate()); row.getCell(6 ).setCellValue(businessDataVO.getNewUsers()); 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()); } ServletOutputStream out = response.getOutputStream(); excel.write(out); out.close(); excel.close(); } catch (IOException e) { e.printStackTrace(); }