整体概述
1).用户层
本项目中在构建系统管理后台的前端页面,我们会用到H5、Vue.js、ElementUI、apache echarts(展示图表)等技术。而在构建移动端应用时,我们会使用到微信小程序
2).网关层
Nginx是一个服务器,主要用来作为Http服务器,部署静态资源,访问性能高。在Nginx中还有两个比较重要的作用: 反向代理和负载均衡, 在进行项目部署时,要实现Tomcat的负载均衡,就可以通过Nginx来实现。
3).应用层
SpringBoot: 快速构建Spring项目, 采用 “约定优于配置” 的思想, 简化Spring项目的配置开发。
SpringMVC:SpringMVC是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。
Spring Task: 由Spring提供的定时任务框架。
httpclient: 主要实现了对http请求的发送。
Spring Cache: 由Spring提供的数据缓存框架
JWT: 用于对应用程序上的用户进行身份验证的标记。
阿里云OSS: 对象存储服务,在项目中主要存储文件,如图片等。
Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试。
POI: 封装了对Excel表格的常用操作。
WebSocket: 一种通信网络协议,使客户端和服务器之间的数据交换更加简单,用于项目的来单、催单功能实现。
4).数据库
MySQL: 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。
Redis: 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存。
Mybatis: 本项目持久层将会使用Mybatis开发。
pagehelper: 分页插件。
spring data redis: 简化java代码操作Redis的API。
模块划分
项目整体分为3个模块
sky-common 模块
constant | 存放相关常用量 |
---|---|
context | 存放上下文管理类 |
enumeration | 项目的枚举类型类 |
exception | 定义了一些常见的异常类型类 |
json | :处理JSON转换的类 |
properties | 存放SpringBoot相关配置类属性 |
result | 存放返回的结果类的封装 |
utils | 存放工具类 |
sky-pojo模块
Entity | 实体,与数据库中的表字段相对应 |
---|---|
DTO | 数据传输对象,通常用于在程序中传递数据 |
VO | 视图展示对象,为前端展示数据提供对象 |
POJO | 普通Java对象,只有属性和对应的getter和setter |
sky-server模块
annotation | 自定义注解 |
---|---|
aspect | 自定义切面类 |
config | 配置类 |
controller | 控制类 |
interceptor | 拦截器 |
mapper | 存放mepper接口 |
service | 服务类 |
task | 定时任务类 |
websocket | websocket服务类 |
数据库模块
分类表category
菜品表dish
菜品口味表dish_flavor
员工表employee
订单详情表order_detail
订单表orders
套餐表setmeal
套餐菜品关系表setmeal_dish
购物车表shopping_cart
用户表user
业务模块
商家端业务功能知识点总结
1.JWT令牌
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用环境间以JSON对象安全地传递信息。JWT的信息可以被验证和信任,因为它是数字签名的。JWT通常用于身份验证和信息交换,广泛应用于现代Web应用和API开发中。
JWT的结构
JWT由三部分组成,分别是**Header(头部)**、Payload(载荷)和Signature(签名),它们之间用点(.
)分隔,格式如下:
Header.Payload.Signature</mark>
2.使用流程
生成JWT的方法
secretKey是密钥,必须保密
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
JWT解密方法
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
员工登录时可以为他生成JWT令牌,存放token
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
// 将员工id放入jwt令牌中 EMP_ID是自定义的
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
// jetProperties是自定义的 通过设置 @Component 注解将其注册为一个Spring Bean 通过
// @ConfigurationProperties(prefix = "sky.jwt") 注解将配置文件中的 sky.jwt 前缀的属性值映射到 JwtProperties 类的属性中
// 然后通过 @Autowired 注解将其注入到 EmployeeController 类中
// 调用 JwtUtil.createJWT 方法生成 JWT 令牌
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
后续要在当前任务中使用用户的ID时,可以通过上下文来获得当前登录用户的token
在拦截器中从请求头中可以获取到当前用户的token,然后通过上面的解析方法来解释当前用户的toekn,接着将用户id存放在BaseContext中:可以看到BaseContext中创建了一个ThreadLocal实现线程的局部变量,它在执行完一个任务之后会结束。用户在当前任务进程中的任务时候,都可以通过BaseContext.getCurrentId来获得当前用户的id.
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
// 解析jwt令牌
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
// 获取jwt令牌中的员工id
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
BaseContext.setCurrentId(empId);
public class BaseContext {
// 创建一个ThreadLocal对象
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
// 设置当前登录用户的id
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
// 获取当前登录用户的id
public static Long getCurrentId() {
return threadLocal.get();
}
// 移除当前登录用户的id
public static void removeCurrentId() {
threadLocal.remove();
}
}
2.拦截器
1.什么是拦截器?
拦截器(Interceptor)是一种特殊的组件,它可以在<mark>请求处理的过程中对请求和响应进行拦截和处理</mark>。拦截器可以在请求到达目标处理器之前、处理器处理请求之后以及视图渲染之前执行特定的操作。<mark>拦截器的主要目的是在不修改原有代码的情况下,实现对请求和响应的统一处理</mark>。
2.拦截器的作用
拦截器可以用于实现以下功能:
权限控制:拦截器可以在请求到达处理器之前进行权限验证,从而实现对不同用户的访问控制。日志记录:拦截器可以在请求处理过程中记录请求和响应的详细信息,便于后期分析和调试。接口幂等性校验:拦截器可以在请求到达处理器之前进行幂等性校验,防止重复提交。数据校验:拦截器可以在请求到达处理器之前对请求数据进行校验,确保数据的合法性。缓存处理:拦截器可以在请求处理之后对响应数据进行缓存,提高系统性能。
3.SpringBoot中拦截器的实现
要在SpringBoot中实现拦截器,首先需要创建一个类并实现HandlerInterceptor接口。HandlerInterceptor接口包含以下三个方法:
preHandle:在请求到达处理器之前执行,可以用于权限验证、数据校验等操作。如果返回true,则继续执行后续操作;如果返回false,则中断请求处理。postHandle:在处理器处理请求之后执行,可以用于日志记录、缓存处理等操作。afterCompletion:在视图渲染之后执行,可以用于资源清理等操作。
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 当前线程的id
System.out.println("当前线程的id:" + Thread.currentThread().getId());
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
// 解析jwt令牌
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
// 获取jwt令牌中的员工id
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
为
为了让拦截器生效我们能还需要将拦截器注册为Bean,在拦截器前加上@Configuration注解然后通过在配置类中注册我们自定义的拦截器。
3.Swagger
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务https://swagger.io它的主要作用是:
使得前后端分离开发更加方便,有利于团队协作
接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
功能测试 Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!
目前,一般都使用knife4j框架。
使用步骤
1.导入 knife4j 的maven坐标在pom.xml中添加依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
2.在配置类中加入 knife4j 相关配置
/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docket1() {
log.info("开始生成接口文档...");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("商家相关接口")
.apiInfo(apiInfo)
.select()
// 指定扫描的包
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
.paths(PathSelectors.any())
.build();
return docket;
}
@Bean
public Docket docket2() {
log.info("开始生成接口文档...");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("用户相关接口")
.apiInfo(apiInfo)
.select()
// 指定扫描的包
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
.paths(PathSelectors.any())
.build();
return docket;
}
3.设置静态资源映射,否则接口文档页面无法访问
/**
* 设置静态资源映射 主要是访问接口文档(html,js,css)
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始设置静态资源映射...");
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
使用是:
@Api(tags = “订单相关接口”)注解:加再Controller类上
4.分页查询
在 Web 应用开发中,分页查询是非常常见的需求,特别是在涉及大量数据的应用场景中,通过分页可以减少数据加载压力,提升系统性能。然而,手动实现分页查询需要编写繁琐的 SQL 语句和逻辑代码,容易出现错误。为了简化分页实现,我们可以借助 PageHelper 这一优秀的分页插件,它能够无缝整合进 Spring Boot 项目,快速实现分页功能。本文将详细介绍如何在 Spring Boot 中整合 PageHelper,并通过示例演示如何进行分页查询。访问网址:[MyBatis 分页插件 PageHelper](https://pagehelper.github.io/
(1).添加PageHelper依赖
首先,在 Spring Boot 项目中的 pom.xml 文件中添加 PageHelper 的依赖项。本文使用 pagehelper-spring-boot-starter 作为依赖包,该依赖能够自动配置 PageHelper,减少手动配置的复杂性。以下是 Maven 依赖的示例:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
(2).设置统一接受对象
为啥要封装一个对象接受呢?
客户端需要两条数据,一个是分页查询的数据,还有一个是分页查询的总条数,但是返回值只能返回一个,因此要封装在一个对象中,代码如下。
/**
* 封装分页查询结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
(3).使用方法
controller层(不变):
接受请求,请求数据=页码+每页数量+查询条件(非必须)调用业务层完成分页查询将结果响应给前端
service层:
调用PageHelper中的 startPage(参数页码,每页数量) 方法,然后返回一个Page对象。调用数据层完成分页查询直接用Page对象中的方法封装结果(数据+数量)返回给controller层
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeeQueryDTO) {
// 使用插件pagehelper (mybatis提供)
// 开始分页查询 1.页码 2.每页记录数
PageHelper.startPage(employeeQueryDTO.getPage(), employeeQueryDTO.getPageSize());
// 2.PageHelper会自动将查询结果封装为Page对象
Page<Employee> page = employeeMapper.pageQuery(employeeQueryDTO);
// 3.等page对象进行处理得到pageresult对象
long total = page.getTotal(); // 总记录数
List<Employee> records = page.getResult(); // 当前页数据列表
return new PageResult(total, records);
}
mapper层:
直接动态SQL拼接带查询条件的查询(SQL语句中不用使用limit)
<!--分页查询-->
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null and name != ''">
and name like concat('%', #{name}, '%')
</if>
</where>
order by create_time desc
</select>
5.MD5加密
数据传输可使用密钥对的方式进行加密解密,使用签名方式验证数据是否可靠,而密码加密存储可使用MD5等一些算法对数据进行单向加密
使用方法直接调用Springboot提供的调用 DigestUtils.md5DigestAsHex
方法进行 MD5 加密,最后返回加密后的十六进制字符串即可
6.AOP技术
我们已经完成了后台系统的**员工管理功能**和**菜品分类功能**的开发,在**新增员工**或者**新增菜品分类**时需要设置创建时间、创建人、修改时间、修改人等字段,在**编辑员工**或者**编辑菜品分类**时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在我们的系统中很多表中都会有这些字段.每次调用service方法时都要执行set方法来设置新的修改时间,代码重复冗余,因此我们可以实现公共字段的填充
实现步骤:
1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
3). 在 Mapper 的方法上加入 AutoFill 注解
自定义注解AutoFill
通过枚举来表示可以进行切面操作的两个数据库的操作方法
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
/**
* 自定义注解 用于标识某个方法需要进行公共字段自动填充处理
*/
@Target({ElementType.METHOD}) // 注解添加的位置
@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期
public @interface AutoFill {
//指定数据库操作的类型 => insert update 可以通过枚举来指定
OperationType value();
}
自定义切面类
/**
* 自定义切面类,实现公共字段自动填充处理逻辑
*/
@Aspect // 标识该类是一个切面类
@Component // 将该类交给Spring管理 标识它是一个bean
@Slf4j // 日志
public class AutoFillAspect {
/**
* 切入点
*/
// 拦截到所有的mapper包下的所有方法 (希望拦截insert和update方法) && 拦截到带有AutoFill注解的方法 ==>实现自动填充功能
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {
}
/**
* 拦截到以后,在通知中进行公共字段的赋值
* @param joinPoint 连接点
* 前置通知
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段自动填充...");
// 1.获取当前拦截的类型是update还是insert
MethodSignature signature =(MethodSignature) joinPoint.getSignature(); // 方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 获取方法上的注解对象
OperationType operationType = autoFill.value(); // 获得当前数据库的操作类型
// 2.获取到方法的实体类
Object[] args = joinPoint.getArgs(); // 获取方法的参数
if(args == null || args.length == 0) { // 判断当前方法的参数是否为空
return;
}
// 规定
Object entity = args[0]; // 获取方法的第一个参数,也就是实体类对象
// 3.为实体对象的公共属性赋值
// 4.准备赋值的数据
LocalDateTime now = LocalDateTime.now(); // 获取当前时间
Long currentId = BaseContext.getCurrentId(); // 获取当前登录用户的id
// 5.更据当前不同的操作类型,为对应的属性通过反射来赋值
if(operationType == OperationType.INSERT) {
// 为4个公共字段赋值 => 通过反射赋值 (获得set方法)
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);
// 通过反射调用set方法
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) {
// 为2个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 通过反射调用set方法
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
在mapper接口上调用注解
/**
* 更据主键动态修改属性
* @param employee
*/
@AutoFill(value = OperationType.UPDATE)
void update(Employee employee);
7.Redis
1.什么是Redis
Redis是一个开源的基于内存的键值对数据库,它的主要特征和作用包括:https://redis.io/
1、基于内存,读写速度极快,可以处理大量读写请求。
2、支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,具有丰富的数据表示能力。 3、支持主从复制,提供数据冗余和故障恢复能力。
4、支持持久化,可以将内存数据保存到磁盘中。
5、支持事务,可以一次执行多个命令。
6、丰富的功能,可用于缓存、消息队列等场景。
主要应用场景包括:
1、缓存常见的使用场景,比如缓存查询结果、热点数据等,大大降低数据库负载。
2、处理大量的读写请求,比如访问统计、消息队列等。
3、排行榜、计数器等功能的实现。
4、pub/sub消息订阅。
5、QUE计划任务
6、分布式锁等。
综上,Redis是一个性能极高的内存数据库,支持丰富数据结构,提供持久化、事务等功能,非常适合缓存、消息队列等场景,被广泛应用于各种大型系统中。它的高性能、丰富功能使其成为非关系型数据库的重要选择之一。
2.常用的redis命令
Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:
- 字符串 string
- 哈希 hash
- 列表 list
- 集合 set
- 有序集合 sorted set / zset
Redis 中字符串类型常用命令:
- SET key value 设置指定key的值
- GET key 获取指定key的值
- SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
- SETNX key value 只有在 key 不存在时设置 key 的值
Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:
- HSET key field value 将哈希表 key 中的字段 field 的值设为 value
- HGET key field 获取存储在哈希表中指定字段的值
- HDEL key field 删除存储在哈希表中的指定字段
- HKEYS key 获取哈希表中所有字段
- HVALS key 获取哈希表中所有值
Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令:
- LPUSH key value1 [value2] 将一个或多个值插入到列表头部
- LRANGE key start stop 获取列表指定范围内的元素
- RPOP key 移除并获取列表最后一个元素
- LLEN key 获取列表长度
- BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止
Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,常用命令:
- SADD key member1 [member2] 向集合添加一个或多个成员
- SMEMBERS key 返回集合中的所有成员
- SCARD key 获取集合的成员数
- SINTER key1 [key2] 返回给定所有集合的交集
- SUNION key1 [key2] 返回所有给定集合的并集
- SREM key member1 [member2] 移除集合中一个或多个成员
Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令:
常用命令:
- ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
- ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
- ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
- ZREM key member [member …] 移除有序集合中的一个或多个成员
Redis的通用命令是不分数据类型的,都可以使用的命令:
- KEYS pattern 查找所有符合给定模式( pattern)的 key
- EXISTS key 检查给定 key 是否存在
- TYPE key 返回 key 所储存的值的类型
- DEL key 该命令用于在 key 存在是删除 key
在Java中操作Redis
Spring Boot提供了对应的Starter,maven坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:
- ValueOperations:string数据操作
- SetOperations:set类型数据操作
- ZSetOperations:zset类型数据操作
- HashOperations:hash类型的数据操作
- ListOperations:list类型的数据操作
环境搭建
1). 导入Spring Data Redis的maven坐标(已完成) org.springframework.boot spring-boot-starter-data-redis
2). 配置Redis数据源
在application-dev.yml中添加 sky: redis: host: localhost port: 6379 password: 123456 database: 10
3). 编写配置类,创建RedisTemplate对象
@Configuration
@Slf4j
public class RedisConfiguration {
// 提供一个方法返回一个模板对象
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("开始创建redisTemplate对象");
// 1.创建一个redisTemplate对象
RedisTemplate redisTemplate = new RedisTemplate();
// 2.与连接工厂对象关联
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 3.设置redis的key序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
Java中实现Redis中的所有操作
public class SpringDataRedisTest {
// 注入redisTemplate
@Autowired
private RedisTemplate redisTemplate;
/**
* 测试redisTemplate
*/
@Test
public void TestRedisTemplate(){
System.out.println(redisTemplate);
// 封装了5类接口
// 1.操作字符串类型
ValueOperations valueOperation = redisTemplate.opsForValue();
// 2.操作Hash类型
HashOperations hashOperation = redisTemplate.opsForHash();
// 3.操作List类型
ListOperations listOperation = redisTemplate.opsForList();
// 4.操作Set类型
SetOperations setOperation = redisTemplate.opsForSet();
// 5.操作ZSet类型
ZSetOperations zSetOperation = redisTemplate.opsForZSet();
}
/**
* 操作字符串类型
*/
@Test
public void TestString(){
// set get set-ex set-nx
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("name","张三");
String name = (String) valueOperations.get("name");
System.out.println(name);
// 设置过期时间
valueOperations.set("code","123654",100, TimeUnit.SECONDS);
// 不存在才设置
valueOperations.setIfAbsent("lock","1");
valueOperations.setIfAbsent("lock","2");
}
/**
* 操作Hash类型
*/
@Test
public void testHash(){
// h-set h-get h-del h-keys h-vals
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("user","name","张三");
hashOperations.put("user","age","18");
String name = (String) hashOperations.get("user","name");
System.out.println(name);
Set keys = hashOperations.keys("user");
System.out.println(keys);
List values = hashOperations.values("user");
System.out.println(values);
hashOperations.delete("user","age");
}
/**
* 操作List类型数据
*/
@Test
public void TestList(){
// lpush lrange rpop llen
ListOperations listOperations = redisTemplate.opsForList();
// 插入多个
listOperations.leftPushAll("mylist","a","b","c","d");
// 插入单个
listOperations.leftPush("mylist","e");
// 查询列表 0 -1 表示查询所有
listOperations.range("mylist",0,-1);
// 输出整个集合
System.out.println("mylist");
// 从列表右侧移除元素
listOperations.rightPop("mylist");
// 获取列表长度
long size = listOperations.size("mylist");
System.out.println(size);
}
/**
* 操作集合类型数据
*/
@Test
public void TestSet(){
// sadd:添加 smembers:查看集合所有成员 scard:元素个数 sinter:交集 sunion:并集 srem:删除元素
SetOperations setOperations = redisTemplate.opsForSet();
setOperations.add("myset1","a","b","c","d");
setOperations.add("myset2","a","b","x","y");
// 获取集合所有成员
Set members = setOperations.members("myset1");
System.out.println(members);
//获取集合大小
long size = setOperations.size("myset1");
System.out.println(size);
// 求交集
Set set = setOperations.intersect("myset1", "myset2");
System.out.println(set);
// 求并集
Set set1 = setOperations.union("myset1", "myset2");
System.out.println(set1);
// 删除元素
setOperations.remove("myset1","a","b");
}
/**
* 操作有序集合
*/
@Test
public void TestZSet(){
// zadd zrange:获取指定范围的数据 zincrby:给某个元素加分数 Zrem
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("myzset","a",100);
zSetOperations.add("myzset","b",80);
zSetOperations.add("myzset","c",90);
zSetOperations.add("myzset","d",70);
// 获取指定范围的数据
Set range = zSetOperations.range("myzset", 0, -1);
System.out.println(range);
// 给某个元素加分数
zSetOperations.incrementScore("myzset","a",10);
// 删除元素
zSetOperations.remove("myzset","a");
}
/**
* 通用命令操作
*/
@Test
public void TestCommon(){
// keys exits type del
Set keys = redisTemplate.keys("*");
System.out.println(keys);
Boolean exists = redisTemplate.hasKey("mylist");
for(Object key:keys){
DataType type = redisTemplate.type(key);
System.out.println(type);
}
redisTemplate.delete("mylist");
}
}
使用:修改店铺状态
调用redisTemplate来设置一个Key对应店铺状态
@PutMapping("{status}")
@ApiOperation("店铺状态修改")
public Result setStatus(@PathVariable Integer status){
log.info("店铺状态修改:{}",status == 1?"营业中":"打烊中");
redisTemplate.opsForValue().set(KEY,status);
return Result.success();
}
查询店铺状态
调用redisTemplate.opsForValue()方法来获得店铺状态
@GetMapping("/status")
@ApiOperation("店铺营业状态查询")
public Result<Integer> getStatus() {
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("店铺营业状态:{}",status == 1?"营业中":"打烊中");
return Result.success(status);
}
注意的是:
application.yml
是一种常用于配置文件的格式,通常用于 Spring Boot 等框架中,有一些格式要求,以确保配置信息能够被正确解析和使用:
- 基本格式
缩进:
application.yml
文件使用缩进来表示层级关系。通常使用两个空格进行缩进,而不是制表符(Tab
)。例如:server:
port: 8080
address: localhost键值对:配置项以键值对的形式存在,键和值之间用冒号(
:
)分隔。冒号后面通常有一个空格,然后是值。例如:spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: password
- 文件结构
顶层配置:文件的顶层可以包含多个顶层配置项,每个配置项之间通过换行分隔。例如:
server:
port: 8080spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: password配置文件的顺序:虽然顺序通常不影响配置的解析,但为了可读性,建议按照逻辑顺序排列配置项。
8.webSocket
1.什么是webSocket
WebSocket是一种协议,用于在Web应用程序和服务器之间建立实时、双向的通信连接。它通过一个单一的TCP连接提供了持久化连接,这使得Web应用程序可以更加实时地传递数据。WebSocket协议最初由W3C开发,并于2011年成为标准。
WebSocket的优势包括:
实时性: 由于WebSocket的持久化连接,它可以实现实时的数据传输,避免了Web应用程序需要不断地发送请求以获取最新数据的情况。
双向通信: WebSocket协议支持双向通信,这意味着服务器可以主动向客户端发送数据,而不需要客户端发送请求。
减少网络负载: 由于WebSocket的持久化连接,它可以减少HTTP请求的数量,从而减少了网络负载。
WebSocket的劣势包括:
需要浏览器和服务器都支持: WebSocket是一种相对新的技术,需要浏览器和服务器都支持。一些旧的浏览器和服务器可能不支持WebSocket。
需要额外的开销: WebSocket需要在服务器上维护长时间的连接,这需要额外的开销,包括内存和CPU。
安全问题: 由于WebSocket允许服务器主动向客户端发送数据,可能会存在安全问题。服务器必须保证只向合法的客户端发送数据。
WebSocket 生命周期描述了 WebSocket 连接从创建到关闭的过程。一个 WebSocket 连接包含以下四个主要阶段:
连接建立阶段(Connection Establishment): 在这个阶段,客户端和服务器之间的 WebSocket 连接被建立。客户端发送一个 WebSocket 握手请求,服务器响应一个握手响应,然后连接就被建立了。
连接开放阶段(Connection Open): 在这个阶段,WebSocket 连接已经建立并开放,客户端和服务器可以在连接上互相发送数据。
连接关闭阶段(Connection Closing): 在这个阶段,一个 WebSocket 连接即将被关闭。它可以被客户端或服务器发起,通过发送一个关闭帧来关闭连接。
连接关闭完成阶段(Connection Closed): 在这个阶段,WebSocket 连接已经完全关闭。客户端和服务器之间的任何交互都将无效。
在Java中使用WebSocket
1.导入依赖坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.使用Java Webocket API编写服务端
/**
* WebSocket服务
*/
@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);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
客户端界面的代码
9.自定义定时任务类
通常,在我们的项目中需要定时给前台发送一些提示性消息或者我们想要的定时信息,这个时候就需要使用定时任务来实现这一功能
首先要在项目启动类上添加注解@EnableScheduling 用来开启任务调度
然后,就可以开始编写一个简单的自定义定时任务的逻辑
@Component
@Slf4j
public class MyTask {
/**
* 定时任务 每5秒执行一次
*/
@Scheduled(cron = "0/5 * * * * ?") // 0/5表示从0秒开始每隔5秒执行一次
public void exectuTask(){
log.info("定时任务执行了");
}
}
@Scheduled(fixedRate = 5000)
:
每隔 5000 毫秒(5 秒)执行一次任务。
任务的执行时间间隔是固定的,不考虑任务的执行时间。
@Scheduled(fixedDelay = 30000)
:
每次任务执行完成后,延迟 30000 毫秒(30 秒)再执行下一次。
任务的延迟时间是从上一次任务完成开始计算的。
@Scheduled(cron = "0 0/30 * * * ?")
:
使用 Cron 表达式定义任务的执行时间。
示例中的 Cron 表达式表示每 30 分钟执行一次。
项目中的运用:定时处理订单任务的状态
1.每天凌晨处理还在派送中的订单
2.每隔5分钟处理超时订单(因为已经派送到了,可能还没有点)
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 超时订单处理
* 定时处理超时订单
* 每分钟出发一次
*/
@Scheduled(cron = "0 * * * * ?") // 每分钟执行一次
public void processTimeoutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());
// select * from orders where status = ? and order_time < (当前时间-15分钟)
List<Orders> list = orderMapper.getByStatusAndOrderTimeLt(Orders.PENDING_PAYMENT, LocalDateTime.now().minusMinutes(15));
// 处理超时订单
if(list != null && list.size() > 0){
for(Orders orders : list){
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("订单超时,自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
}
/**
* 处理一直处于派送中的订单
* 每天凌晨1点出发一次
*/
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行一次
public void processDeliveryOrder(){
log.info("定时处理一直处于派送中的订单:{}", LocalDateTime.now());
// select * from orders where status =? and order_time < (当前时间-60分钟)
List<Orders> list = orderMapper.getByStatusAndOrderTimeLt(Orders.DELIVERY_IN_PROGRESS, LocalDateTime.now().minusMinutes(60));
// 处理超时订单
if(list!= null && list.size() > 0){
for(Orders orders : list){
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
}
}
}
}