sky-take-out
项目介绍
功能架构
技术选型
MD5
1.修改数据库中明文密码,改为MD5加密后的密文
2.修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对
Swagger
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。
员工管理
面试问题
- nginx反向代理好处
1 | 提高访问速度 |
- DTO
1 | 当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据 |
- JWT
- 日期处理
在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
- 公共字段自动填充
使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。
实现步骤:
1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
3). 在 Mapper 的方法上加入 AutoFill 注解
技术点:枚举、注解、AOP、反射
- 新增菜品,包括图片文件
使用第三方的存储服务,选用了阿里云的OSS服务进行文件存储。
- 删除菜品的性能优化
在DishServiceImpl中,删除菜品是一条一条传送执行的,大大降低了执行效率,为了提高性能,进行修改,使用动态sql执行删除操作
- redis
基于键值对的非关系数据库。
常用数据类型
1 | 字符串 string |
- 微信登录
业务规则:
基于微信登录实现小程序的登录功能
如果是新用户需要自动完成注册
要完成微信登录的话,最终就要获得微信用户的openid。在小程序端获取授权码后,向后端服务发送请求,并携带授权码,这样后端服务在收到授权码后,就可以去请求微信接口服务。最终,后端向小程序返回openid和token等数据。
- 应用层
SpringBoot: 快速构建Spring项目, 采用 “约定优于配置” 的思想, 简化Spring项目的配置开发。
SpringMVC:SpringMVC是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。
Spring Task: 由Spring提供的定时任务框架。
httpclient: 主要实现了对http请求的发送。
Spring Cache: 由Spring提供的数据缓存框架
JWT: 用于对应用程序上的用户进行身份验证的标记。
阿里云OSS: 对象存储服务,在项目中主要存储文件,如图片等。
Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试。
POI: 封装了对Excel表格的常用操作。
WebSocket: 一种通信网络协议,使客户端和服务器之间的数据交换更加简单,用于项目的来单、催单功能实现。
- 数据层
MySQL: 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。
Redis: 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存。
Mybatis: 本项目持久层将会使用Mybatis开发。
- Swagger
- 使得前后端分离开发更加方便,有利于团队协作
- 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
- 功能测试
- 解析出登录员工id后,如何传递给Service的save方法
通过ThreadLocal进行传递。
- 分页
使用 mybatis 的分页插件 PageHelper 来简化分页代码的开发。
1 | PageHelper是MyBatis的一个插件,内部实现了一个PageInterceptor拦截器。Mybatis会加载这个拦截器到拦截器链中。在我们使用过程中先使用PageHelper.startPage这样的语句在当前线程上下文中设置一个ThreadLocal变量,再利用PageInterceptor这个分页拦截器拦截,从ThreadLocal中拿到分页的信息,如果有分页信息拼装分页SQL(limit语句等)进行分页查询,最后再把ThreadLocal中的东西清除掉。 |
- 前端小程序的微信登录流程
1 | 微信登录的核心是通过微信小程序提供的临时凭证code换取永久凭证openid的过程 |
- redis应用
1 | 我们项目中有两处地方用到了Redis,分别是:店铺营业状态标识和小程序端的套餐、菜品列表数据 |
- SpringCache在项目中的应用
1 | SpringCache是Spring提供的一个缓存框架,它可以通过简单的注解实现缓存的操作,我们常用的注解有下面几个: |
- SpringTask在项目中的应用
1 | SpringTask是Spring框架提供的一种任务调度工具,用来按照定义的时间格式执行某段代码。 |
cron表达式其实就是一个字符串,通过cron表达式可以定义任务的触发时间
- WebSocket对比HTTP
1 | HTTP的通信是单向的,要先请求后响应,类似于对讲机 |
- 核心功能
菜品新增:对菜品表和口味表进行新增操作
首先将前端传过来的菜品信息保存到菜品表并主键返回,然后遍历前端传过来的口味集合,
为每个口味设置刚才返回来的主键并保存到口味表
菜品修改:对菜品表进行更新,对菜品详情表进行增删操作
首先根据前端传过来的菜品信息对菜品表进行修改
然后根据菜品id删除对应的口味列表集合
最后再把前端传过来的口味集合重新加入到口味表中
菜品删除:对菜品表和口味表进行删除操作
遍历前端传过来的菜品id集合得到每个菜品的信息
如果当前菜品是启售状态或者被套餐关联那么就不能被删除
否则就可以通过菜品id对菜品表和口味表中的数据进行删除
套餐新增:对套餐表和套餐菜品关系表进行新增操作
首先将前端传过来的套餐基本信息保存到套餐表中,并返回主键的id
然后为前端传过来的套餐菜品设置套餐id
最后将套餐包含的菜品添加到套餐菜品关系表中
套餐修改:对套餐表进行修改,在对套餐菜品关系表进行增删操作
首先根据前端传过来的套餐基本信息更新到套餐表中
然后根据套餐的id删除所有套餐菜品关系表中包含的菜品信息
最后遍历前端传过来的菜品的列表,设置好套餐的id后重新保存到套餐菜品关系表中
套餐删除:对套餐表和套餐菜品关系表进行删除操作
首先遍历前端传过来套餐id的集合得到每一个套餐的信息
然后根据id查询套餐,判断套餐的状态是否为启售状态,如果是启售状态,则不能删除
如果是在禁售状态,就可以通过套餐的id进行套餐菜品关系表的删除操作
分类删除
分类删除的核心逻辑就是根据前端传过来的分类id去分类表进行一个删除操作
但是要对这个分类里面是否有菜品和套餐做一个判断,拿着这个id去菜品表和套餐表做一个统计查询
如果查出来数量大于0,就不能删除,如果为0,直接删除
添加购物车:将用户选择的商品基本数据信息添加到数据库表中进行保存
利用到的数据库表(本次项目):购物车表,菜品表、套餐表,保存的信息就是从表中查到的
首先根据id查询购物车中是否有相同商品
有:则不用添加,只修改查询到的商品number属性+1并重新赋值即可,执行mapper更新。
无:则判断是菜品还是套餐,查询对应商品的数据库得到基本信息,补全购物车需要的参数执行保存。
- websocket
- threadlocal
项目困难
JAVA面试题
Redis
使用场景
缓存
缓存穿透
- 定义:查询一个不存在数据,每次查询都请求数据库
- 解决方案一:缓存空数据。
优点:简单
缺点:消耗内存,可能发生不一致问题
- 方案二:布隆过滤器
优点:内存占用少,没有多余key
缺点:实现复杂,存在误判
缓存击穿
- 定义:给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能会瞬间把DB压垮。
- 解决方案一:互斥锁
可以保证数据强一致性,但是性能较差
- 方案二:逻辑过期
性能较高,不能保证数据绝对一致。
缓存雪崩
- 定义:同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库
双写一致性
一致性高:
读写锁效率更高一点。
一致性要求不高:
允许延时一致。
持久化
- RDB
- AOF
数据过期策略
- 惰性删除
- 定时删除
Redis的过期策略是二者结合
数据淘汰策略
分布式锁
redisson实现的分布式锁-可重入。
其他
Redis集群
主从复制
全量同步:
增量同步:
哨兵
分片集群
其他
MYSQL
优化
定位慢查询
如何分析
索引
覆盖索引
超大分页
索引创建原则
索引失效
经验
- 表的设计优化
- SQL语句优化
- 主从复制、读写分离
事务
特性
并发事务
解决方法
log
MVCC
主从同步原理
分库分表
框架
Spring
bean
spring中的单例bean不是线程安全的。
AOP
- 定义
事务失效场景
bean的生命周期
循环引用
SpringMVC执行流程
Springboot自动配置原理
自动装配,简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需要开发人员再去写 Bean 的装配配置。
在 Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就可以实现自动装配。
常见注解
MyBatis
执行流程
延迟加载
一、二级缓存
集合
List
数组
ArrayList源码
ArrayList底层原理
数组和List之间的转换
HashMap
- 散列表
实现原理
PUT
扩容机制
寻址算法
多线程
线程基础
进程和线程
创建线程方式
状态
顺序执行
wait和sleep
如何终止线程
线程安全
synchronized底层原理
底层实现是Monitor
一旦锁发生竞争,都会升级为重量级锁。
JMM
CAS
volatile
- 保证线程间可见性
- 禁止指令重排序
AQS
AQS核⼼思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是⽤CLH队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。
ReentrantLock
synchronized和Lock
死锁
ConcurrentHashMap
并发问题
线程池
线程池核心参数
常见阻塞队列
如何缺点核心线程数
线程池种类
使用场景
JVM
JVM组成
程序计数器
java堆
虚拟机栈
方法区
直接内存
类装载过程
垃圾回收
何时回收
引用计数法可能会循环引用,造成泄露,所以现在经常用可达性分析。
垃圾回收算法
分代回收
引用
内存泄露排查思路
CPU排查思路
企业场景
设计模式
工厂设计模式
策略模式
责任链模式
技术场景
单点登录
权限认证
上传数据安全性
棘手问题
采集日志
怎么快速定位系统瓶颈
补充1
基础
- 重载和重写
- 装箱和拆箱
- == 与 equals
- hashCode 与 equals
- Java 中的异常处理
- Java 序列化中如果有些字段不想进⾏序列化,怎么办?
对于不想进⾏序列化的变量,使⽤ transient 关键字修饰。
transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法。
Java 中 IO 流
NIO:基于同步非阻塞 I/O 模型。尽管 NIO 支持非阻塞模式,但所有的 I/O 操作最终都是同步完成的。这意味着当一个 I/O 操作被发起时,它不会立即返回结果,而是需要应用程序通过轮询或选择器来检查操作是否完成。
AIO:真正的异步 I/O 模型。在 AIO 中,所有的 I/O 操作都是异步完成的。一旦发起一个 I/O 请求,该请求将立即返回,允许程序继续执行其他任务。操作系统会在 I/O 操作完成后通知应用程序。
- 集合框架底层数据结构总结
- 进程和线程
- 死锁
- 为什么我们调⽤ start() ⽅法时会执⾏run() ⽅法,为什么我们不能直接调⽤run() ⽅法
- 双重校验锁实现对象单例(线程安全)
- java对象创建过程
- 对象的访问定位有哪两种⽅式?
- 常⻅的垃圾回收器有那些?
Serial收集器
Serial(串⾏)收集器收集器是最基本、历史最悠久的垃圾收集器了。⼤家看名字就知道这个收集器是⼀个单线程收集器了。它的 单线程 的意义不仅仅意味着它只会使⽤⼀条垃圾收集线程去完成垃圾收集⼯作,更重要的是它在进⾏垃圾收集⼯作的时候必须暂停其他所有的⼯作线程( “Stop The World”),直到它收集结束。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控参数、收集算法、回收策略等等)和Serial收集器完全⼀样。
Parallel Scavenge收集器
- 双亲委派模型
- ⼀条SQL语句在MySQL中如何执⾏的
- 索引实现为什么是B树而不是红黑树
- 受检异常和非受检异常
- b+而不是b
- 守护线程
- mysql索引类型
- springboot约定优先于配置
- Spring中事务的传播行为
- Dubbo如何动态感知服务下线
当 Dubbo 服务提供方出现故障导致 Zookeeper 剔除了这个服务的地址,
- Mybatis 中#{}和${}的区别是什么?
- 数据库连接池
数据库连接池是一种池化技术,池化技术的核心思想是实现资源的复用,避免资源重复创建销毁的开销。
而在数据库的应用场景里面,应用程序每次向数据库发起 CRUD 操作的时候,都需要创建连接在数据库访问量较大的情况下,频繁的创建连接会带来较大的性能开销。
(如图)而连接池的核心思想,就是应用程序在启动的时候提前初始化一部分连接保存到连接池里面,当应用需要使用连接的时候,直接从连接池获取一个已经建立好的链接。连接池的设计,避免了每次连接的建立和释放带来的开销。
- new String(“abc”)到底创建了几个对象?
- String、StringBuffer、StringBuilder 区别
- JVM年龄代为什么是15次
一个对象的GC年龄是储存在对象头里的,而对象头里有4位储存GC年龄,最大值为15。
- 深拷贝和浅拷贝
- Spring IOC 和 DI
- finally块一定执行吗
- Integer 和 int 的区别
- 零拷贝
- 在2G文件中,找出高频top100
- 表数据量大的时候,影响查询效率的主要原因
- 两个 Integer 对象比较大小,为什么 100 等于 100,1000 不等于 1000
因为Intefer的valueOf方法,判断时如果目标值在-128-127则会直接从cache取值。
- MQ(消息中间件)
MQ 全称是 Message Queue,直译过来叫做消息队列,主要是作为分布式应用之间实现异步通信的方式。
主要由三个部分组成,分别是生产者、消息服务端和消费者
流量消峰
应用解耦
异步处理
- 给你 ab,ac,abc 字段,你是如何加索引 的
- Dubbo的服务请求失败怎么处理
- 如何实现Redis和Mysql的一致性
当应用程序需要去读取某个数据的时候,首先会先尝试去 Redis 里面加载,如果命中就直接返回。如果没有命中,就从数据库查询,查询到数据后再把这个数据缓存到 Redis里面。
在这种情况下,能够选择的方法只有几种。
先更新数据库,再更新缓存
先删除缓存,再更新数据库
如果先更新数据库,再更新缓存,如果缓存更新失败,就会导致数据库和 Redis 中的数据不一致。
如果是先删除缓存,再更新数据库,理想情况是应用下次访问 Redis 的时候,发现 Redis里面的数据是空的,就从数据库加载保存到 Redis 里面,那么数据是一致的。但是在极端情况下,由于删除 Redis 和更新数据库这两个操作并不是原子的,所以这个过程如果有其他线程来访问,还是会存在数据不一致问题。
所以,如果需要在极端情况下仍然保证 Redis 和 Mysql 的数据一致性,就只能采用最终一致性方案。(如图)比如基于 RocketMQ 的可靠性消息通信,来实现最终一致性。
因为这里是基于最终一致性来实现的,如果业务场景不能接受数据的短期不一致性,那就不能使用这个方案来做。
- Dubbo核心功能
- Dubbo工作原理
- Mysql优化
RPC核心代码
基础
编写基于Vert.x实现的web服务器VertxHttpServer,能够监听指定端口并处理请求
1 | package com.yupi.yurpc.server; |
本地服务注册中心
1 | package com.yupi.yurpc.registry; |
请求处理器
1 | package com.yupi.yurpc.server; |
TCP版本
1 | package com.yupi.yurpc.server.tcp; |
请求发送
1 | package com.yupi.yurpc.proxy; |
序列化器
序列化器JSON
JSON序列化器需要考虑对象转换的兼容问题,主要是因为Java语言中的泛型擦除(Type Erasure)机制和JSON数据格式本身的特性。
- 泛型擦除: 在Java中,泛型信息在编译后会被擦除,这意味着运行时无法直接获取泛型参数的具体类型信息。例如,
List<String>
和List<Integer>
在运行时都被视为List
类型。当使用Jackson等库进行反序列化时,默认情况下它们可能不知道如何将JSON对象映射回原始的Java泛型类型,可能会默认返回如LinkedHashMap
这样的类型。 - JSON与Java对象模型差异: JSON是一种轻量级的数据交换格式,它没有像Java那样的复杂类型系统。JSON只支持几种基本类型(字符串、数字、布尔值、数组、对象和null)。因此,当从JSON反序列化到Java对象时,有时候JSON结构并不能完全对应Java类的结构,特别是对于复杂的嵌套对象或自定义类型。
Kryo 是一个专门为Java设计的高效序列化框架,它与JSON序列化器如Jackson相比,在处理对象转换兼容性问题上有不同的特性,这使得Kryo在某些情况下不需要特别考虑类型转换的兼容问题。
1 | package com.yupi.yurpc.serializer; |
kryo序列器
1 | package com.yupi.yurpc.serializer; |
hessian
1 | package com.yupi.yurpc.serializer; |
注册中心
- 由于一个服务可能有多个提供者,有两种设计结构
对于ZooKeeper和Etcd这种支持层级查询的中间件,用第一种更加清晰。
注册信息定义
1 | package com.yupi.yurpc.model; |
1 | /** |
注册
1 | @Override |
假设有一个服务实例:
serviceName
: “my-service”serviceVersion
: “1.0” (默认值)serviceHost
: “192.168.1.10”servicePort
: 8080
服务发现
1 | public List<ServiceMetaInfo> serviceDiscovery(String serviceKey) { |
这段代码实现了基于 etcd 的服务发现功能,具体步骤如下:
- 构建查询前缀:根据给定的服务键构建一个特定的路径前缀。
- 配置查询选项:设置查询条件为前缀匹配。
- 执行查询:向 etcd 发起查询请求,获取所有匹配的服务实例。
- 解析结果:将查询结果中的每个键值对的值部分(JSON 字符串)反序列化为
ServiceMetaInfo
对象。 - 返回结果:将所有解析后的
ServiceMetaInfo
对象收集到一个列表中并返回。 - 异常处理:确保在出现错误时能够适当处理,并提供有用的错误信息。
心跳检测和续期机制
1 | @Override |
利用重新注册实现续签,定时任务是通过 CronUtil
类来完成的,来自于Hutool
服务节点下线
本地缓存,用一个列表来实现
自定义协议
自定义协议后TCP服务器
1 | package com.yupi.yurpc.server.tcp; |
TCP客户端
1 | package com.yupi.yurpc.server.tcp; |
消息编码器与解码器
半包粘包
RecordParse的作用是:保证下次读到特定长度的字符。
负载均衡
轮询
1 | package com.yupi.yurpc.loadbalancer; |
随机
1 |
|
一致性Hash
1 | package com.yupi.yurpc.loadbalancer; |
重试机制
不重试
1 | package com.yupi.yurpc.fault.retry; |
固定时间重试
1 | package com.yupi.yurpc.fault.retry; |
容错机制
手写RPC框架
手写RPC框架
简易版本RPC开发
基本概念
什么是RPC
RPC:Remote Procedure Call即远程过程调用,是一种计算机通信协议,允许程序在不同计算机之间进行通信和交互,就像本地调用一样。
为什么需要RPC
- 透明性:RPC 提供了一种机制,使得远程服务调用看起来就像本地方法调用一样。开发者不需要关心底层的网络通信细节,可以专注于业务逻辑。
- 封装性:RPC 框架通常会处理序列化、反序列化、网络传输等复杂操作,将这些细节从应用程序中抽象出来。
- 模块化:通过将服务拆分为多个独立的服务,并使用 RPC 进行通信,可以实现更好的模块化。每个服务都可以独立开发、部署和扩展。
- 解耦合:服务之间的依赖关系可以通过接口定义来管理,而不是直接的代码依赖。这使得系统的各个部分更加松散耦合,更容易进行修改和升级。
RPC框架实现思路
基本设计
假设有消费者和服务提供者两个角色
消费者想要调用提供者,就需要提供者启动一个web服务,然后通过请求客户端发送HTTP请求或者其他协议来调用。
若有多个服务和方法,每个接口都要单独写一个接口,则较为麻烦。
则提供一个统一的服务调用接口,通过请求处理器根据客户端的请求参数来进行不同的处理、调用不同的服务和方法。
可以在服务提供者维护一个本地服务注册器,记录服务和对应实现类的映射。
另外,需要注意的是,java对象在网络中无法直接传输,所以需要对传输的参数进行序列化和反序列化。
所以,一个简易的RPC框架就此诞生。
开发简易版RPC框架
简易版实现几个模块:
- example-common:示例代码的公共依赖,包括接口、model等
- example-consumer:示例服务消费者代码
- example-provide:示例服务t提供者代码
- lkj-rpc-easy:简易版rpc框架
公共模块
- 编写实体类User
1 | package com.lkj.example.common.model; |
对象需要实习序列化接口,为后续网络传输序列化提供支持
- 编写用户服务接口UserService,提供一个获取用户的方法
1 | package com.lkj.example.common.service; |
服务提供者
- 编写服务实现类,实现公共模块中定义的用户服务接口
功能是打印用户的名称,并且返回参数中的用户对象
1 | package com.lkj.example.provider; |
- 编写服务提供者启动类
1 | package com.lkj.example.provider; |
服务消费者
- 创建服务消费者启动类,编写调用接口的代码
1 | package com.lkj.example.consumer; |
这里我们还没有获取userService实例,所以定义为null
web服务器
要让服务提供者提供可远程访问的服务,那么就需要一个web服务器,接受处理并返回请求。
此处使用高性能的NIO框架Vert.x作为RPC框架的web服务器。
- 编写一个web服务器的接口HttpServer,定义统一的启动服务器方法
1 | package com.lkj.yurpc.server; |
- 编写基于Vert.x的web服务器VertxHttpServer,能够箭筒指定端口并处理请求
1 | package com.lkj.yurpc.server; |
本地服务注册器
在简易版本中,暂时不用第三方注册中心,直接把服务注册到服务提供者本地。
- 使用线程安全的ConcurrentHashMap存储服务注册信息,key为服务名称,value为服务的实现类
1 | package com.lkj.yurpc.registry; |
序列化器
- 序列化:将java对象转为可传输的字节数组
- 反序列化:将字节数组转换为可传输的java对象
为了方便此处选择java原生的序列化器。
1 | package com.lkj.yurpc.serializer; |
提供者调用-请求处理器
- RpcRequest的作用是封装调用所需的信息,比如服务名称,方法名称,调用参数的类型列表,参数列表。
1 | package com.lkj.yurpc.model; |
- 响应类RpcResponse的作用是封装调用方法得到的返回值、以及调用的信息等。
1 | package com.lkj.yurpc.model; |
- 编写请求处理器 HttpServerHandler
业务流程如下:
1.反序列化请求为对象,并从请求对象中获取参数
2.根据服务名称从本地注册器中获取到对应的服务实现类
3.通过反射机制调用方法,得到返回结果
4.对返回结果进行封装和序列化,并写入响应
1 | package com.lkj.yurpc.server; |
消费方发起调用-代理
代理的实现方式大致分为2类:静态代理和动态代理
静态代理
静态代理是指为每一个特定类型的接口或对象,编写一个代理类。
静态代理虽然很好理解,但缺点也很明显,如果给每个服务接口都写一个实现类,是非常麻烦的,代理方式灵活性很差。
所以RPC框架中,会使用动态代理。
动态代理
动态代理的作用是,根据要生成的对象类型,自动生成一个代理对象。
常用的动态代理实现方式有JDK动态代理和基于字节码生成的动态代理。前者简单易用、无需引入额外的库,但缺点是只能对接口进行代理;后者更灵活、可以对任何类进行代理,但性能略低于JDK动态代理。
1 | package com.lkj.yurpc.proxy; |
当用户调用某个接口的方法时,会改为调用invoke方法。在invoke方法中,我们可以获取到要调用的方法信息,传入的参数列表等。
需要注意的时,上述代码中,请求的服务提供者地址被硬编码了,需要使用注册中心和服务发现机制来解决。
- 创建动态代理工厂ServiceProxyFactory,作用是根据指定类创建动态代理对象
1 | package com.lkj.yurpc.proxy; |
简易测试
静态代理的调试过程
- 直接跳到20行,开始调用getuser
- 进入UserServiceProxy的getUser方法
- 执行到http请求,进入拦截器部分,即第35行
- 开始执行拦截器,进行反射调用返回结果,中间进行序列化反序列化等操作
- 返回代理中,得到结果
动态代理调试过程
InvocationHandler
接口:ServiceProxy
类实现了InvocationHandler
接口,这是 Java 动态代理的核心接口。invoke
方法:invoke
方法是InvocationHandler
接口中的唯一方法。这个方法会在每次通过代理对象调用方法时被调用。invoke方法接收三个参数:
proxy
:代理对象本身。method
:被调用的方法。args
:方法的参数。
动态代理的工作流程
- 构造请求:
invoke
方法首先构建一个RpcRequest
对象,包含服务名、方法名、参数类型和参数值。 - 序列化请求:将
RpcRequest
对象序列化为字节数组。 - 发送请求:使用 HTTP 请求库(如 Hutool)将序列化的请求发送到指定的服务器地址(这里是硬编码的
http://localhost:8080
)。 - 接收响应:从服务器接收响应,并将其反序列化为
RpcResponse
对象。 - 返回结果:从
RpcResponse
中提取数据并返回给调用者。
与静态代理的区别
静态代理
- 手动创建代理类:静态代理需要为每个接口手动创建一个代理类,代理类实现了相同的接口。
- 编译时确定:静态代理在编译时就已经确定,代理类的代码是固定的。
- 灵活性差:如果接口发生变化,需要手动修改代理类,维护成本高。
动态代理
- 自动生成代理类:动态代理在运行时自动生成代理类,不需要手动编写代理类。
- 运行时确定:动态代理在运行时根据实际调用的方法动态生成代理类。
- 灵活性高:动态代理可以灵活地添加或修改拦截逻辑,适用于多种场景,特别是当接口经常变化时。
由于动态代理中,invoke方法会在每次通过代理对象调用方法时被调用。拦截器调用次数多,调试较为繁琐,所以不做图片展示。
全局配置加载
需求分析
在RPC框架的运行中,会涉及到很多的配置信息,比如注册中心的地址,序列化方式,网络服务器端口号等等。
在简易版本的RPC项目中,我们采用了硬编码的方式,不利于维护。
同时PRC框架是需要被其他项目作为提供者或消费者引入的,所以我们应当允许引入框架的项目编写配置文件来自定义配置。
因此,需要配置一套全局配置加载功能。
项目实现
配置加载
- 新建RpcConfig,用于保存配置信息
1 | package com.lkj.yurpc.config; |
- 新建工具类ConfigUtils,作用是读配置文件并返回配置对象
1 | package com.lkj.yurpc.utils; |
- 新建RpcConstant接口,用于存储RPC框架相关常量
1 | package com.lkj.yurpc.constant; |
维护全局配置对象
RPC框架中需要维护一个全局的配置对象,在引入RPC框架的项目启动时,从配置文件中读取配置并创建对象实例,之后就可以集中的从这个对象获取配置信息,而不是每次加载配置时重新读取配置,并创建新的对象,减少了性能开销。
使用设计模式中的单例模式。
单例模式(Singleton Pattern)是设计模式中的一种创建型模式,它确保一个类只有一个实例,并提供一个全局访问点。单例模式的主要目的是控制共享资源的访问,例如数据库连接或线程池。
单例模式的特点
- 唯一实例:确保一个类只有一个实例。
- 全局访问点:提供一个全局的访问点来获取这个唯一的实例。
- 延迟初始化:可以实现懒加载(Lazy Initialization),即在第一次使用时才创建实例。
- 线程安全:在多线程环境下,必须保证单例的创建是线程安全的。
一般情况下,我们会使用holder来维护全局配置对象实例。
1 | package com.lkj.yurpc; |
测试
在example-consumer项目的resourses目录编写配置文件application.properties
1 | rpc.name=yurpc |
创建ConsumerExample作为扩展后RPC项目的示例消费者类,测试配置文件读取。
1 | import com.lkj.yurpc.config.RpcConfig; |
接口Mock
需求分析
什么是Mock
RPC框架的核心功能是调用其他远程服务。但是在实际开发和测试过程中,有时候可能无法访问真实的远程服务,并且远程服务可能产生不可控的影响,例如网络延迟,服务不稳定等。在这种情况下,就需要使用mock服务来模拟远程服务的行为,以便进行接口的测试开发和调试。
mock是指模拟对象,通常用于测试代码中,特别是在单元测试中,便于我们跑通业务流程。
设计方案
通过动态代理创建一个调用方法时返回固定值的对象。
开发实现
- 首先给全局配置类RpcConfig新增mock字段,默认值为false
- 在Proxy包下新增MockServiceProxy类,用于生成mock代理方法。
在这个类中,需要提供一个根据服务接口类型返回固定值的方法。
1 | package com.lkj.yurpc.proxy; |
- 给ServiceProxyFactory服务代理工厂新增获取mock代理对象的方法getMockProxy。
1 | /** |
测试
- 在UserService中写个具有默认实现的新方法
1 | package com.lkj.example.common.service; |
- 修改ConsumerExample类,编写调用的新方法
1 | public class ConsumerExample { |
可以看到输出的结果为0,而不是1,说明调用了模拟服务代理。
SPI
- SPI(Service Provider Interface) 服务提供接口是Java的机制,主要用于实现模块化开发和插件化开发扩展。
- SPI机制允许服务提供者通过特定的配置文件将自己的实现注册到系统,然后系统通过反射机制动态加载这些实现,而不需要修改原始框架的代码,从而实现了系统的解耦、提高了可扩展性。
一个典型的SPI应用场景是JDBC
- 编写SpiLoder加载器
相当于一个工具类,提供了读取配置并加载实现类的方法。
关键实现如下:
- 用map来存储已知的配置信息,键名->实现类
- 扫描指定路径,读取每个配置文件,获取到键名->实现类信息并存储在map中
- 定义获取实例方法,根据用户传入的接口和键名,从map中寻找对应的实现类,然后通过反射获取到实现类对象。可以维护一个对象实例缓存,创建过一次的对象从缓存中读取即可。
1 | package com.lkj.yurpc.spi; |
注册中心基本实现
需求分析
RPC框架的一个核心模块式注册中心,其目的是帮助服务消费者获取到服务提供者的调用地址,而不是把调用地址硬编码到项目中
设计方案
注册中心核心功能
- 数据分布式存储:集中的注册信息数据存储、读取和共享
- 服务注册:服务提供者上报服务信息到注册中心
- 服务发现:服务消费者从注册中心拉取服务信息
- 心跳检测:定期检查服务提供者的存活状态
- 服务注销:手动剔除节点,或者自动删除失效节点
技术选型
- 需要一个能够集中存储和读取数据的中间件。还需要有数据过期,数据监听的能力,使我们移除失效节点,更新节点列表。
ETCD
数据结构
采用层次化的键值对来存储数据,支持类似于文件系统路径的层次结构
核心结构:
- key:Etcd的基本数据单元,类似于文件系统的文件名。每个键都唯一标识一个值,并且可以包含子键,形成于类似路径的层次结构。
- value:与键关联的数据,通常为字符型
ETCD核心特性:
- Lease(租约):用于对键值对进行TTL超时设置,即设置键值对的过期时间。
- Watch(监听):可以监视特定键的变化,当键的值发生变化时,会触发相应通知。
开发实现
注册中心开发
- 注册信息定义
在model包下新建ServiceMetaInfo类,封装服务的注册信息,包括服务名称、服务版本号、服务地址和服务分组等。
1 | package com.lkj.yurpc.model; |
- 注册中心配置
在config包下编写注册中心配置类RegistryConfig,让用户配置连接注册中心所需的信息,比如注册中心类别、注册中心地址、用户名、密码、连接超时等。
1 | package com.lkj.yurpc.config; |
- 注册中心接口
遵循可扩展机制,先写一个注册中心接口,后续可以实现多种不同的注册中心,使用SPI机制动态加载
1 | package com.lkj.yurpc.registry; |
- etcd注册中心实现
1 | package com.lkj.yurpc.registry; |
支持配置和扩展注册中心
一个成熟的rpc框架可能会支持多个注册中心,我们的需求是,让开发者能够填写配置来指定使用的注册中心,并且支持自定义注册中心
- 注册中心常量
1 | package com.lkj.yurpc.registry; |
- 工厂模式
1 | public class RegistryFactory { |
- 修改ServiceProxy类
[ ] ```
package com.lkj.yurpc.proxy;import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.lkj.yurpc.RpcApplication;
import com.lkj.yurpc.config.RpcConfig;
import com.lkj.yurpc.constant.RpcConstant;
import com.lkj.yurpc.model.RpcRequest;
import com.lkj.yurpc.model.RpcResponse;
import com.lkj.yurpc.model.ServiceMetaInfo;
import com.lkj.yurpc.registry.Registry;
import com.lkj.yurpc.registry.RegistryFactory;
import com.lkj.yurpc.serializer.Serializer;
import com.lkj.yurpc.serializer.SerializerFactory;import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;/**
服务代理(JDK 动态代理)
**/
public class ServiceProxy implements InvocationHandler {/**
- 调用代理
* - @return
@throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 指定序列化器
final Serializer serializer = SerializerFactory.getInstance(RpcApplication.getRpcConfig().getSerializer());// 构造请求
String serviceName = method.getDeclaringClass().getName();
RpcRequest rpcRequest = RpcRequest.builder().serviceName(serviceName) .methodName(method.getName()) .parameterTypes(method.getParameterTypes()) .args(args) .build();
try {
// 序列化 byte[] bodyBytes = serializer.serialize(rpcRequest); // 从注册中心获取服务提供者请求地址 RpcConfig rpcConfig = RpcApplication.getRpcConfig(); Registry registry = RegistryFactory.getInstance(rpcConfig.getRegistryConfig().getRegistry()); ServiceMetaInfo serviceMetaInfo = new ServiceMetaInfo(); serviceMetaInfo.setServiceName(serviceName); serviceMetaInfo.setServiceVersion(RpcConstant.DEFAULT_SERVICE_VERSION); List<ServiceMetaInfo> serviceMetaInfoList = registry.serviceDiscovery(serviceMetaInfo.getServiceKey()); if (CollUtil.isEmpty(serviceMetaInfoList)) { throw new RuntimeException("暂无服务地址"); } ServiceMetaInfo selectedServiceMetaInfo = serviceMetaInfoList.get(0); // 发送请求 try (HttpResponse httpResponse = HttpRequest.post(selectedServiceMetaInfo.getServiceAddress()) .body(bodyBytes) .execute()) { byte[] result = httpResponse.bodyBytes(); // 反序列化 RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class); return rpcResponse.getData(); }
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
- 调用代理
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
## 注册中心优化
### 需求分析
之前我们完成了基础的注册中心,能够注册和获取服务和节点信息。
对于数据一致性、性能优化、高可用性、可扩展性还有很多优化空间。
### 注册中心优化
#### 心跳检测和持续机制
##### 介绍
心跳检测是一种用于检测系统是否正常工作的机制。通过定期发送心跳信号来检测目标系统的状态。
##### 方案设计
实现心跳检测一般需要两个关键:定时、网络请求
因为etcd自带了key过期机制,我们可以给节点注册信息一个生命倒计时,让节点定期续期,重置自己的倒计时。如果一直不重置,则删除。
* 服务提供者向etcd注册自己的服务信息,并在注册时设置TTL
* etcd在收到信息后,自动维护ttl,并在ttl过期时删除
* 服务提供者定期请求etcd续签自己的注册信息,重写ttl
##### 开发实现
* 给注册中心补充心跳检测方法/**
- 心跳检测(服务端)
*/
void heartBeat();/**1
2
3
* 维护续期节点集合 本机注册的节点 key 集合(用于维护续期)
*/
private final SetlocalRegisterNodeKeySet = new HashSet<>(); 1
2
3
* 实现hearBeat方法@Override
public void heartBeat() {
// 10 秒续签一次
CronUtil.schedule(“/10 “, new Task() {@Override public void execute() { // 遍历本节点所有的 key for (String key : localRegisterNodeKeySet) { try { List<KeyValue> keyValues = kvClient.get(ByteSequence.from(key, StandardCharsets.UTF_8)) .get() .getKvs(); // 该节点已过期(需要重启节点才能重新注册) if (CollUtil.isEmpty(keyValues)) { continue; } // 节点未过期,重新注册(相当于续签) KeyValue keyValue = keyValues.get(0); String value = keyValue.getValue().toString(StandardCharsets.UTF_8); ServiceMetaInfo serviceMetaInfo = JSONUtil.toBean(value, ServiceMetaInfo.class); register(serviceMetaInfo); } catch (Exception e) { throw new RuntimeException(key + "续签失败", e); } } }
});
// 支持秒级别定时任务
CronUtil.setMatchSecond(true);
CronUtil.start();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#### 服务节点下线机制
当服务提供者节点宕机时,应该从注册中心移除已注册的节点。
##### 方案设计
服务节点下线又分为:
* 主动下线:服务提供者项目正常退出
* 被动下线:异常退出
被动下线已经实现,现在开发主动下线。
利用JVM的ShutdownHook机制。
##### 开发实现
* 完善destorypublic void destroy() {
System.out.println(“当前节点下线”);
// 下线节点
// 遍历本节点所有的 key
for (String key : localRegisterNodeKeySet) {
try {kvClient.delete(ByteSequence.from(key, StandardCharsets.UTF_8)).get();
} catch (Exception e) {
throw new RuntimeException(key + "节点下线失败");
}
}// 释放资源
if (kvClient != null) {
kvClient.close();
}
if (client != null) {
client.close();
}
}
1 |
|
public static void init(RpcConfig newRpcConfig) {
rpcConfig = newRpcConfig;
log.info(“rpc init, config = {}”, newRpcConfig.toString());
// 注册中心初始化
RegistryConfig registryConfig = rpcConfig.getRegistryConfig();
Registry registry = RegistryFactory.getInstance(registryConfig.getRegistry());
registry.init(registryConfig);
log.info(“registry init, config = {}”, registryConfig);
// 创建并注册 Shutdown Hook,JVM 退出时执行操作
Runtime.getRuntime().addShutdownHook(new Thread(registry::destroy));
}
1 |
|
package com.lkj.yurpc.protocol;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
- 协议消息结构
*
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProtocolMessage
/**
* 消息头
*/
private Header header;
/**
* 消息体(请求或响应对象)
*/
private T body;
/**
* 协议消息头
*/
@Data
public static class Header {
/**
* 魔数,保证安全性
*/
private byte magic;
/**
* 版本号
*/
private byte version;
/**
* 序列化器
*/
private byte serializer;
/**
* 消息类型(请求 / 响应)
*/
private byte type;
/**
* 状态
*/
private byte status;
/**
* 请求 id
*/
private long requestId;
/**
* 消息体长度
*/
private int bodyLength;
}
}
1 |
|
package com.lkj.yurpc.protocol;
/**
协议常量
**/
public interface ProtocolConstant {/**
消息头长度
*/
int MESSAGE_HEADER_LENGTH = 17;/**
协议魔数
*/
byte PROTOCOL_MAGIC = 0x1;/**
- 协议版本号
*/
byte PROTOCOL_VERSION = 0x1;
}
1 |
|
package com.lkj.yurpc.protocol;
import lombok.Getter;
/**
- 协议消息的状态枚举
@Getter
public enum ProtocolMessageStatusEnum {
OK("ok", 20),
BAD_REQUEST("badRequest", 40),
BAD_RESPONSE("badResponse", 50);
private final String text;
private final int value;
ProtocolMessageStatusEnum(String text, int value) {
this.text = text;
this.value = value;
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static ProtocolMessageStatusEnum getEnumByValue(int value) {
for (ProtocolMessageStatusEnum anEnum : ProtocolMessageStatusEnum.values()) {
if (anEnum.value == value) {
return anEnum;
}
}
return null;
}
}1
2
3
* 协议信息类型枚举,包括请求响应心跳等。
package com.lkj.yurpc.protocol;
import lombok.Getter;
/**
协议消息的类型枚举
**/
@Getter
public enum ProtocolMessageTypeEnum {REQUEST(0),
RESPONSE(1),
HEART_BEAT(2),
OTHERS(3);private final int key;
ProtocolMessageTypeEnum(int key) {
this.key = key;
}
/**
- 根据 key 获取枚举
* - @param key
- @return
*/
public static ProtocolMessageTypeEnum getEnumByKey(int key) {
for (ProtocolMessageTypeEnum anEnum : ProtocolMessageTypeEnum.values()) {
}if (anEnum.key == key) { return anEnum; }
return null;
}
}package com.lkj.yurpc.protocol;1
2
3
* 协议消息的序列化器枚举
- 根据 key 获取枚举
import cn.hutool.core.util.ObjectUtil;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
@Getter
public enum ProtocolMessageSerializerEnum {
JDK(0, "jdk"),
JSON(1, "json"),
KRYO(2, "kryo"),
HESSIAN(3, "hessian");
private final int key;
private final String value;
ProtocolMessageSerializerEnum(int key, String value) {
this.key = key;
this.value = value;
}
/**
* 获取值列表
*
* @return
*/
public static List<String> getValues() {
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
}
/**
* 根据 key 获取枚举
*
* @param key
* @return
*/
public static ProtocolMessageSerializerEnum getEnumByKey(int key) {
for (ProtocolMessageSerializerEnum anEnum : ProtocolMessageSerializerEnum.values()) {
if (anEnum.key == key) {
return anEnum;
}
}
return null;
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static ProtocolMessageSerializerEnum getEnumByValue(String value) {
if (ObjectUtil.isEmpty(value)) {
return null;
}
for (ProtocolMessageSerializerEnum anEnum : ProtocolMessageSerializerEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
}1
2
3
4
5
6
7
8
9
#### 网络传输
新建server.tcp包,将所有TCP服务相关的代码放到该包中
* TCP服务器实现
新建VertcTcpServer类
package com.lkj.yurpc.server.tcp;
import io.vertx.core.Vertx;
import io.vertx.core.net.NetServer;
import lombok.extern.slf4j.Slf4j;
/**
Vertx TCP 服务器
**/
@Slf4j
public class VertxTcpServer {public void doStart(int port) {
// 创建 Vert.x 实例 Vertx vertx = Vertx.vertx(); // 创建 TCP 服务器 NetServer server = vertx.createNetServer(); // 处理请求 server.connectHandler(new TcpServerHandler()); // 启动 TCP 服务器并监听指定端口 server.listen(port, result -> { if (result.succeeded()) { log.info("TCP server started on port " + port); } else { log.info("Failed to start TCP server: " + result.cause()); } });
}
public static void main(String[] args) {
new VertxTcpServer().doStart(8888);
}
}
1 |
|
package com.lkj.yurpc.server.tcp;
import cn.hutool.core.util.IdUtil;
import com.yupi.yurpc.RpcApplication;
import com.yupi.yurpc.model.RpcRequest;
import com.yupi.yurpc.model.RpcResponse;
import com.yupi.yurpc.model.ServiceMetaInfo;
import com.yupi.yurpc.protocol.*;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetSocket;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
Vertx TCP 请求客户端
**/
public class VertxTcpClient {/**
- 发送请求
* - @param rpcRequest
- @param serviceMetaInfo
- @return
- @throws InterruptedException
@throws ExecutionException
*/
public static RpcResponse doRequest(RpcRequest rpcRequest, ServiceMetaInfo serviceMetaInfo) throws InterruptedException, ExecutionException {
// 发送 TCP 请求
Vertx vertx = Vertx.vertx();
NetClient netClient = vertx.createNetClient();
CompletableFutureresponseFuture = new CompletableFuture<>();
netClient.connect(serviceMetaInfo.getServicePort(), serviceMetaInfo.getServiceHost(),result -> { if (!result.succeeded()) { System.err.println("Failed to connect to TCP server"); return; } NetSocket socket = result.result(); // 发送数据 // 构造消息 ProtocolMessage<RpcRequest> protocolMessage = new ProtocolMessage<>(); ProtocolMessage.Header header = new ProtocolMessage.Header(); header.setMagic(ProtocolConstant.PROTOCOL_MAGIC); header.setVersion(ProtocolConstant.PROTOCOL_VERSION); header.setSerializer((byte) ProtocolMessageSerializerEnum.getEnumByValue(RpcApplication.getRpcConfig().getSerializer()).getKey()); header.setType((byte) ProtocolMessageTypeEnum.REQUEST.getKey()); // 生成全局请求 ID header.setRequestId(IdUtil.getSnowflakeNextId()); protocolMessage.setHeader(header); protocolMessage.setBody(rpcRequest); // 编码请求 try { Buffer encodeBuffer = ProtocolMessageEncoder.encode(protocolMessage); socket.write(encodeBuffer); } catch (IOException e) { throw new RuntimeException("协议消息编码错误"); } // 接收响应 TcpBufferHandlerWrapper bufferHandlerWrapper = new TcpBufferHandlerWrapper( buffer -> { try { ProtocolMessage<RpcResponse> rpcResponseProtocolMessage = (ProtocolMessage<RpcResponse>) ProtocolMessageDecoder.decode(buffer); responseFuture.complete(rpcResponseProtocolMessage.getBody()); } catch (IOException e) { throw new RuntimeException("协议消息解码错误"); } } ); socket.handler(bufferHandlerWrapper); });
RpcResponse rpcResponse = responseFuture.get();
// 记得关闭连接
netClient.close();
return rpcResponse;
}
}
- 发送请求
1 |
|
package com.lkj.yurpc.protocol;
import com.lkj.yurpc.serializer.Serializer;
import com.lkj.yurpc.serializer.SerializerFactory;
import io.vertx.core.buffer.Buffer;
import java.io.IOException;
/**
- 协议消息编码器
public class ProtocolMessageEncoder {
/**
* 编码
*
* @param protocolMessage
* @return
* @throws IOException
*/
public static Buffer encode(ProtocolMessage<?> protocolMessage) throws IOException {
if (protocolMessage == null || protocolMessage.getHeader() == null) {
return Buffer.buffer();
}
ProtocolMessage.Header header = protocolMessage.getHeader();
// 依次向缓冲区写入字节
Buffer buffer = Buffer.buffer();
buffer.appendByte(header.getMagic());
buffer.appendByte(header.getVersion());
buffer.appendByte(header.getSerializer());
buffer.appendByte(header.getType());
buffer.appendByte(header.getStatus());
buffer.appendLong(header.getRequestId());
// 获取序列化器
ProtocolMessageSerializerEnum serializerEnum = ProtocolMessageSerializerEnum.getEnumByKey(header.getSerializer());
if (serializerEnum == null) {
throw new RuntimeException("序列化协议不存在");
}
Serializer serializer = SerializerFactory.getInstance(serializerEnum.getValue());
byte[] bodyBytes = serializer.serialize(protocolMessage.getBody());
// 写入 body 长度和数据
buffer.appendInt(bodyBytes.length);
buffer.appendBytes(bodyBytes);
return buffer;
}
}
1 |
|
package com.lkj.yurpc.protocol;
import com.lkj.yurpc.model.RpcRequest;
import com.lkj.yurpc.model.RpcResponse;
import com.lkj.yurpc.serializer.Serializer;
import com.lkj.yurpc.serializer.SerializerFactory;
import io.vertx.core.buffer.Buffer;
import java.io.IOException;
/**
协议消息解码器
*/
public class ProtocolMessageDecoder {/**
- 解码
* - @param buffer
- @return
- @throws IOException
*/
public static ProtocolMessage<?> decode(Buffer buffer) throws IOException {
// 分别从指定位置读出 Buffer
ProtocolMessage.Header header = new ProtocolMessage.Header();
byte magic = buffer.getByte(0);
// 校验魔数
if (magic != ProtocolConstant.PROTOCOL_MAGIC) {
}throw new RuntimeException("消息 magic 非法");
header.setMagic(magic);
header.setVersion(buffer.getByte(1));
header.setSerializer(buffer.getByte(2));
header.setType(buffer.getByte(3));
header.setStatus(buffer.getByte(4));
header.setRequestId(buffer.getLong(5));
header.setBodyLength(buffer.getInt(13));
// 解决粘包问题,只读指定长度的数据
byte[] bodyBytes = buffer.getBytes(17, 17 + header.getBodyLength());
// 解析消息体
ProtocolMessageSerializerEnum serializerEnum = ProtocolMessageSerializerEnum.getEnumByKey(header.getSerializer());
if (serializerEnum == null) {
}throw new RuntimeException("序列化消息的协议不存在");
Serializer serializer = SerializerFactory.getInstance(serializerEnum.getValue());
ProtocolMessageTypeEnum messageTypeEnum = ProtocolMessageTypeEnum.getEnumByKey(header.getType());
if (messageTypeEnum == null) {
}throw new RuntimeException("序列化消息的类型不存在");
switch (messageTypeEnum) {
}case REQUEST: RpcRequest request = serializer.deserialize(bodyBytes, RpcRequest.class); return new ProtocolMessage<>(header, request); case RESPONSE: RpcResponse response = serializer.deserialize(bodyBytes, RpcResponse.class); return new ProtocolMessage<>(header, response); case HEART_BEAT: case OTHERS: default: throw new RuntimeException("暂不支持该消息类型");
}
- 解码
}
1 |
|
package com.lkj.yurpc.server.tcp;
import com.lkj.yurpc.model.RpcRequest;
import com.lkj.yurpc.model.RpcResponse;
import com.lkj.yurpc.protocol.*;
import com.lkj.yurpc.registry.LocalRegistry;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.NetSocket;
import java.io.IOException;
import java.lang.reflect.Method;
/**
TCP 请求处理器
*/
public class TcpServerHandler implements Handler{ /**
- 处理请求
* @param socket the event to handle
*/
@Override
public void handle(NetSocket socket) {
TcpBufferHandlerWrapper bufferHandlerWrapper = new TcpBufferHandlerWrapper(buffer -> {// 接受请求,解码 ProtocolMessage<RpcRequest> protocolMessage; try { protocolMessage = (ProtocolMessage<RpcRequest>) ProtocolMessageDecoder.decode(buffer); } catch (IOException e) { throw new RuntimeException("协议消息解码错误"); } RpcRequest rpcRequest = protocolMessage.getBody(); ProtocolMessage.Header header = protocolMessage.getHeader(); // 处理请求 // 构造响应结果对象 RpcResponse rpcResponse = new RpcResponse(); try { // 获取要调用的服务实现类,通过反射调用 Class<?> implClass = LocalRegistry.get(rpcRequest.getServiceName()); Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes()); Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs()); // 封装返回结果 rpcResponse.setData(result); rpcResponse.setDataType(method.getReturnType()); rpcResponse.setMessage("ok"); } catch (Exception e) { e.printStackTrace(); rpcResponse.setMessage(e.getMessage()); rpcResponse.setException(e); } // 发送响应,编码 header.setType((byte) ProtocolMessageTypeEnum.RESPONSE.getKey()); header.setStatus((byte) ProtocolMessageStatusEnum.OK.getValue()); ProtocolMessage<RpcResponse> responseProtocolMessage = new ProtocolMessage<>(header, rpcResponse); try { Buffer encode = ProtocolMessageEncoder.encode(responseProtocolMessage); socket.write(encode); } catch (IOException e) { throw new RuntimeException("协议消息编码错误"); }
});
socket.handler(bufferHandlerWrapper);
}
- 处理请求
}
1 |
|
package com.lkj.yurpc.server.tcp;
import com.lkj.yurpc.protocol.ProtocolConstant;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.parsetools.RecordParser;
/**
- TCP 消息处理器包装
装饰者模式,使用 recordParser 对原有的 buffer 处理能力进行增强
*/
public class TcpBufferHandlerWrapper implements Handler{ /**
解析器,用于解决半包、粘包问题
*/
private final RecordParser recordParser;public TcpBufferHandlerWrapper(Handler
bufferHandler) {
recordParser = initRecordParser(bufferHandler);
}@Override
public void handle(Buffer buffer) {
recordParser.handle(buffer);
}/**
- 初始化解析器
* - @param bufferHandler
@return
*/
private RecordParser initRecordParser(HandlerbufferHandler) {
// 构造 parser
RecordParser parser = RecordParser.newFixed(ProtocolConstant.MESSAGE_HEADER_LENGTH);parser.setOutput(new Handler
() { // 初始化 int size = -1; // 一次完整的读取(头 + 体) Buffer resultBuffer = Buffer.buffer(); @Override public void handle(Buffer buffer) { // 1. 每次循环,首先读取消息头 if (-1 == size) { // 读取消息体长度 size = buffer.getInt(13); parser.fixedSizeMode(size); // 写入头信息到结果 resultBuffer.appendBuffer(buffer); } else { // 2. 然后读取消息体 // 写入体信息到结果 resultBuffer.appendBuffer(buffer); // 已拼接为完整 Buffer,执行处理 bufferHandler.handle(resultBuffer); // 重置一轮 parser.fixedSizeMode(ProtocolConstant.MESSAGE_HEADER_LENGTH); size = -1; resultBuffer = Buffer.buffer(); } }
});
return parser;
}
}
1 |
|
// 第一次
Hello, server!Hello, server!Hello, server!Hello, server!
// 第二次
Hello, server!Hello, server!Hello, server!Hello, server!
1 |
|
// 第一次
Hello, server!Hello, server!
// 第二次
Hello, server!Hello, server!Hello, server!
1 |
|
// 第三次
Hello, server!Hello, server!Hello, server!Hello, server!Hello, server!
1 |
|
if (buffer == null || buffer.length() == 0) {
throw new RuntimeException(“消息 buffer 为空”);
}
if (buffer.getBytes().length < ProtocolConstant.MESSAGE_HEADER_LENGTH) {
throw new RuntimeException(“出现了半包问题”);
}
1 |
|
// 解决粘包问题,只读指定长度的数据
byte[] bodyBytes = buffer.getBytes(17, 17 + header.getBodyLength());
1 |
|
package com.lkj.yurpc.loadbalancer;
import com.lkj.yurpc.model.ServiceMetaInfo;
import java.util.List;
import java.util.Map;
/**
负载均衡器(消费端使用)
**/
public interface LoadBalancer {/**
- 选择服务调用
* - @param requestParams 请求参数
- @param serviceMetaInfoList 可用服务列表
- @return
*/
ServiceMetaInfo select(MaprequestParams, List serviceMetaInfoList);
}
- 选择服务调用
1 |
|
package com.lkj.yurpc.loadbalancer;
import com.lkj.yurpc.model.ServiceMetaInfo;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
轮询负载均衡器
*/
public class RoundRobinLoadBalancer implements LoadBalancer {/**
当前轮询的下标
*/
private final AtomicInteger currentIndex = new AtomicInteger(0);@Override
public ServiceMetaInfo select(MaprequestParams, List serviceMetaInfoList) {
if (serviceMetaInfoList.isEmpty()) {return null;
}
// 只有一个服务,无需轮询
int size = serviceMetaInfoList.size();
if (size == 1) {return serviceMetaInfoList.get(0);
}
// 取模算法轮询
int index = currentIndex.getAndIncrement() % size;
return serviceMetaInfoList.get(index);
}
}
1 |
|
package com.lkj.yurpc.loadbalancer;
import com.lkj.yurpc.model.ServiceMetaInfo;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
随机负载均衡器
*/
public class RandomLoadBalancer implements LoadBalancer {private final Random random = new Random();
@Override
public ServiceMetaInfo select(MaprequestParams, List serviceMetaInfoList) { int size = serviceMetaInfoList.size(); if (size == 0) { return null; } // 只有 1 个服务,不用随机 if (size == 1) { return serviceMetaInfoList.get(0); } return serviceMetaInfoList.get(random.nextInt(size));
}
}1
2
3
* 实现一致性 Hash 负载均衡器package com.lkj.yurpc.loadbalancer;
import com.lkj.yurpc.model.ServiceMetaInfo;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
一致性哈希负载均衡器
**/
public class ConsistentHashLoadBalancer implements LoadBalancer {/**
一致性 Hash 环,存放虚拟节点
*/
private final TreeMapvirtualNodes = new TreeMap<>(); /**
虚拟节点数
*/
private static final int VIRTUAL_NODE_NUM = 100;@Override
public ServiceMetaInfo select(MaprequestParams, List serviceMetaInfoList) {
if (serviceMetaInfoList.isEmpty()) {return null;
}
// 构建虚拟节点环
for (ServiceMetaInfo serviceMetaInfo : serviceMetaInfoList) {for (int i = 0; i < VIRTUAL_NODE_NUM; i++) { int hash = getHash(serviceMetaInfo.getServiceAddress() + "#" + i); virtualNodes.put(hash, serviceMetaInfo); }
}
// 获取调用请求的 hash 值
int hash = getHash(requestParams);// 选择最接近且大于等于调用请求 hash 值的虚拟节点
Map.Entryentry = virtualNodes.ceilingEntry(hash);
if (entry == null) {// 如果没有大于等于调用请求 hash 值的虚拟节点,则返回环首部的节点 entry = virtualNodes.firstEntry();
}
return entry.getValue();
}
/**
* Hash 算法,可自行实现
*
* @param key
* @return
*/
private int getHash(Object key) {
return key.hashCode();
}
}
1 |
|
package com.lkj.yurpc.loadbalancer;
/**
负载均衡器键名常量
**/
public interface LoadBalancerKeys {/**
轮询
*/
String ROUND_ROBIN = “roundRobin”;String RANDOM = “random”;
String CONSISTENT_HASH = “consistentHash”;
}
1 |
|
package com.lkj.yurpc.loadbalancer;
import com.lkj.yurpc.spi.SpiLoader;
/**
负载均衡器工厂(工厂模式,用于获取负载均衡器对象)
**/
public class LoadBalancerFactory {static {
SpiLoader.load(LoadBalancer.class);
}
/**
默认负载均衡器
*/
private static final LoadBalancer DEFAULT_LOAD_BALANCER = new RoundRobinLoadBalancer();/**
- 获取实例
* - @param key
- @return
*/
public static LoadBalancer getInstance(String key) {
return SpiLoader.getInstance(LoadBalancer.class, key);
}
}
1 |
|
/**
* 负载均衡器
*/
private String loadBalancer = LoadBalancerKeys.ROUND_ROBIN;
1 |
|
package com.lkj.yurpc.fault.retry;
import com.lkj.yurpc.model.RpcResponse;
import java.util.concurrent.Callable;
/**
重试策略
**/
public interface RetryStrategy {/**
- 重试
* - @param callable
- @return
- @throws Exception
*/
RpcResponse doRetry(Callablecallable) throws Exception;
}
- 重试
1 |
|
package com.lkj.yurpc.fault.retry;
import com.lkj.yurpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
/**
不重试 - 重试策略
**/
@Slf4j
public class NoRetryStrategy implements RetryStrategy {/**
- 重试
* - @param callable
- @return
- @throws Exception
*/
public RpcResponse doRetry(Callablecallable) throws Exception {
return callable.call();
}
- 重试
}
1 |
|
package com.lkj.yurpc.fault.retry;
import com.github.rholder.retry.*;
import com.lkj.yurpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
固定时间间隔 - 重试策略
**/
@Slf4j
public class FixedIntervalRetryStrategy implements RetryStrategy {/**
- 重试
* - @param callable
- @return
- @throws ExecutionException
- @throws RetryException
*/
public RpcResponse doRetry(Callablecallable) throws ExecutionException, RetryException {
Retryerretryer = RetryerBuilder. newBuilder()
return retryer.call(callable);.retryIfExceptionOfType(Exception.class) .withWaitStrategy(WaitStrategies.fixedWait(3L, TimeUnit.SECONDS)) .withStopStrategy(StopStrategies.stopAfterAttempt(3)) .withRetryListener(new RetryListener() { @Override public <V> void onRetry(Attempt<V> attempt) { log.info("重试次数 {}", attempt.getAttemptNumber()); } }) .build();
}
- 重试
}
1 |
|
package com.lkj.yurpc.fault.retry;
/**
重试策略键名常量
**/
public interface RetryStrategyKeys {/**
不重试
*/
String NO = “no”;/**
- 固定时间间隔
*/
String FIXED_INTERVAL = “fixedInterval”;
}
1 |
|
package com.lkj.yurpc.fault.retry;
import com.lkj.yurpc.spi.SpiLoader;
/**
重试策略工厂(用于获取重试器对象)
**/
public class RetryStrategyFactory {static {
SpiLoader.load(RetryStrategy.class);
}
/**
默认重试器
*/
private static final RetryStrategy DEFAULT_RETRY_STRATEGY = new NoRetryStrategy();/**
- 获取实例
* - @param key
- @return
*/
public static RetryStrategy getInstance(String key) {
return SpiLoader.getInstance(RetryStrategy.class, key);
}
}
1 |
|
// 使用重试机制
RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(rpcConfig.getRetryStrategy());
RpcResponse rpcResponse = retryStrategy.doRetry(() ->
VertxTcpClient.doRequest(rpcRequest, selectedServiceMetaInfo)
);
1 |
|
package com.lkj.yurpc.fault.tolerant;
import com.lkj.yurpc.model.RpcResponse;
import java.util.Map;
/**
容错策略
**/
public interface TolerantStrategy {/**
- 容错
* - @param context 上下文,用于传递数据
- @param e 异常
- @return
*/
RpcResponse doTolerant(Mapcontext, Exception e);
}package com.lkj.yurpc.fault.tolerant;1
2
3
4
5
* 快速失败容错策略实现
遇到异常后,将异常再次抛出,交给外层实现
- 容错
import com.lkj.yurpc.model.RpcResponse;
import java.util.Map;
/**
快速失败 - 容错策略(立刻通知外层调用方)
**/
public class FailFastTolerantStrategy implements TolerantStrategy {@Override
public RpcResponse doTolerant(Mapcontext, Exception e) { throw new RuntimeException("服务报错", e);
}
}
1 |
|
package com.lkj.yurpc.fault.tolerant;
import com.lkj.yurpc.model.RpcResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
静默处理异常 - 容错策略
**/
@Slf4j
public class FailSafeTolerantStrategy implements TolerantStrategy {@Override
public RpcResponse doTolerant(Mapcontext, Exception e) { log.info("静默处理异常", e); return new RpcResponse();
}
}
1 |
|
package com.lkj.yurpc.fault.tolerant;
/**
容错策略键名常量
**/
public interface TolerantStrategyKeys {/**
故障恢复
*/
// String FAIL_BACK = “failBack”;/**
快速失败
*/
String FAIL_FAST = “failFast”;/**
故障转移
*/
// String FAIL_OVER = “failOver”;/**
- 静默处理
*/
String FAIL_SAFE = “failSafe”;
}
1 |
|
package com.lkj.yurpc.fault.tolerant;
import com.lkj.yurpc.spi.SpiLoader;
/**
容错策略工厂(工厂模式,用于获取容错策略对象)
**/
public class TolerantStrategyFactory {static {
SpiLoader.load(TolerantStrategy.class);
}
/**
默认容错策略
*/
private static final TolerantStrategy DEFAULT_RETRY_STRATEGY = new FailFastTolerantStrategy();/**
- 获取实例
* - @param key
- @return
*/
public static TolerantStrategy getInstance(String key) {
return SpiLoader.getInstance(TolerantStrategy.class, key);
}
}
1 |
|
// rpc 请求
// 使用重试机制
RpcResponse rpcResponse;
try {
RetryStrategy retryStrategy = RetryStrategyFactory.getInstance(rpcConfig.getRetryStrategy());
rpcResponse = retryStrategy.doRetry(() ->
VertxTcpClient.doRequest(rpcRequest, selectedServiceMetaInfo)
);
} catch (Exception e) {
// 容错机制
TolerantStrategy tolerantStrategy = TolerantStrategyFactory.getInstance(rpcConfig.getTolerantStrategy());
rpcResponse = tolerantStrategy.doTolerant(null, e);
}
return rpcResponse.getData();
```
智能面试刷题平台项目
智能面试刷题平台项目
项目介绍
于 Next.js 服务端渲染 + Spring Boot + Redis + MySQL + Elasticsearch 的 面试刷题平台。
管理员可以创建题库、题目和题解,并批量关联题目到题库;用户可以注册登录、分词检索题目、在线刷题并查看刷题记录日历等。
采用了一些企业级的新技术,比如使用数据库连接池、热 Key 探测、缓存、高级数据结构来提升性能。通过流量控制、熔断、动态 IP 黑白名单过滤、同端登录冲突检测、分级反爬虫策略来提升系统和内容的安全性。
项目结构设计图:
技术选型
后端
- Java Spring Boot 框架 + Maven 多模块构建
- MySQL 数据库 + MyBatis-Plus 框架 + MyBatis X
- Redis 分布式缓存 + Caffeine 本地缓存
- Redission 分布式锁 + BitMap + BloomFilter
- ⭐️ Elasticsearch 搜索引擎
- ⭐️ Druid 数据库连接池 + 并发编程
- ⭐️ Sa-Token 权限控制
- ⭐️ HotKey 热点探测
- ⭐️ Sentinel 流量控制
- ⭐️ Nacos 配置中心
- ⭐️ 多角度项目优化:性能、安全性、可用性
前端
- React 18 框架
- ⭐️ Next.js 服务端渲染
- ⭐️ Redux 状态管理
- Ant Design 组件库
- 富文本编辑器组件
- ⭐️ 前端工程化:ESLint + Prettier + TypeScript
- ⭐️ OpenAPI 前端代码生成
OJ在线判题系统
实习
实习
java
字符串
String 类提供两种查找字符串的方法
1 | indexOf()和lastIndexOf() |
1 | charAt()返回指定索引的字符 |
1 | str.substring(int beginIndex) |
判断字符串是否相等
1 | str.equals(String s)//区分大小写 |
1 | compareTo()//按字典顺序比较字符串 |
字符串分割
1 | str.split(String sign )//sign为分隔符 |
类和对象
==与equals()
1 | ==是比较地址,equals比较内容 |
对象销毁
1 | 对象引用超过作用范围 |
包装类
Integer
1 | Integer a=new Integer(7); |
Untitled
Web前端
HTML
块元素与行内元素
超链接
div 组织块,无实际内容
快捷
1 | .content(内容名称) |
span组织行内元素
表单
1 | placeholder输入内容自动消失 |
input标签type
要实现单选,需要加一个name属性,同时name名称必须相同
1 | label中的for元素是与input绑定,与ID对应,所以不适用于单选 |
form中的action就是规定提交到哪一个url
CSS
CSS
1为直接放入html标签
2为放入头部
3为放入一个专门的css文件夹
优先级
CSS选择器
元素选择器
类选择器
class控制类
ID选择器
创建包含id的快捷方式
1 | #+id |
通用选择器
CSS常用属性
复合属性
行内元素,块内元素,行内块元素可以相互转换
利用display
盒子模型
1 | borde-width遵循上右下左原则 |
浮动
清除浮动:overflow
定位
相对定位
JS
简介
JS数据与基本变量
const是常量;var是变量,作用于函数作用域;let是变量,作用于块级作用域。let更加安全灵活。
JS控制语句
JS函数
JS事件
JS DOM
响应式布局
FLEX布局
微信公众号开发
微信web开发者平台
KND项目说明
KND项目说明
项目内容
项目计划完成一个网页界面,所实现功能为在原有基础上,当下拉框选择了前面部分的信息后,后面的电缆信息会自动给出,同时后面信息在自动给出的基础上,也可下拉调整。
项目实现思路
对于整体项目实现,主要难度体现在后面电缆信息自动给出部分。我所采取的解决办法为:建立表名为“约束”的表,在对于后面电缆信息的每一栏,采取数据库查询,然后展示查询内容,所采取的查询原理为,首先在表“约束”中查询有无和前面信息都匹配的元素,然后UNION ALL该栏中本该展示的元素,这样即可实现对于匹配的信息优先给出,又可自行选择的效果。
以下为一个示例:
1 | --反馈电缆_X轴 |
以反馈电缆_X轴为例,对于后面需要实现的每一项,都编写了对应的查询函数来实现。
项目具体实现说明
以下是完成工作介绍。
数据库部分
首先是对于所有表项的创建,文件名为:
1 | 创建.sql |
然后进行了一些测试元素的插入,文件名为:
1 | 插入.sql |
对于表约束的创建,文件名为:
1 | 约束.sql |
同时创建了表记录每次插入的一组信息,文件名为:
1 | 记录.sql |
因为前面部分,如系统部分,驱动部分,选择件部分的下拉框显示,在前端绑定即可实现,所以没有编写sql函数。
而对于后面的部分,各种电缆部分,因为是被选择部分,所以需要运用数据库查询语句,文件夹名为查询,其中对应的sql文件即为对应的查询语句。
前端部分
因为技术和时间原因,前端部分目前实现了系统部分,驱动部分与选择件部分的展示和数据库连接。
在KND文件下,文件
1 | knd.qspx |
即为对应的前端界面,其余为测试部分。
未实现部分
在整体项目中,后面部分的前端展示未实现,但是对应的查询语句已经完成。
计算机设计大赛
计算机设计大赛
微课与教学辅助赛道介绍
微课与教学辅助赛道是中国大学生计算机设计大赛中的一个重要类别,该类别的参赛作品主要是针对教学辅助领域的应用和创新。
参赛作品的形式主要包括以下几种:
微课视频:参赛者可以根据自己的教学经验和所学专业知识,制作适合初学者学习的微课视频。该类作品要求内容丰富、表达清晰、形式新颖,同时需要具有一定的教学实用性和可行性。
教学辅助工具:参赛者可以自主开发基于计算机技术的教学辅助工具,如教学管理系统、在线作业系统、学生评价系统等。该类作品要求具有一定的实用性和创新性,能够为教师和学生提供便捷的教学辅助服务。
教育游戏:参赛者可以开发基于计算机技术的教育游戏,旨在通过游戏化的方式提高学生对知识的兴趣和学习效果。该类作品要求游戏性强、教育性强,能够帮助学生快乐地学习。
教学资源共享平台:参赛者可以开发教学资源共享平台,旨在为教师和学生提供便捷的资源共享服务。该类作品要求平台功能完善、资源丰富,能够满足教学需求。
参赛作品的形式不限于上述几种,只要能够创新应用计算机技术为教学服务,满足比赛要求即可。
1 | 注:你们做的应该是属于微课视频类,下面着重介绍一下微课视频类。 |
微课视频
微课视频是中国大学生计算机设计大赛中微课与教学辅助赛道的一个参赛作品形式。微课视频主要是指通过视频方式来呈现教学内容,让学生通过视听学习的方式获取知识。微课视频的表现形式可以有以下几种:
1 | 1. PPT课件+录音:这种形式主要是将PPT课件和讲解录音结合起来,通过录音对PPT课件进行讲解。这种形式操作简单,制作成本较低,适合教学内容相对简单的课程。 |
除了以上几种形式外,还有一些其他的微课视频表现形式,例如模拟演示、VR课程等,参赛者可以根据自己的创新想法和实际情况选择适合自己的表现形式。
中国大学生计算机设计大赛微课与教学辅助赛道的国家级奖项获得者形式多种多样,没有一种形式是绝对更容易获奖的。根据历届大赛的获奖情况来看,微课赛道的国家级奖项获得者包括了视频课件、虚拟实验、在线答疑等不同类型的作品。不同类型的作品在获奖的比重上并没有明显的差别,获奖的关键还是在作品的设计创新性、实用性、技术难度等方面,同时还需要具有良好的学术论证和实际应用价值。
1 | 注意:这里设计创新性肯定是要有的,实用性应该都具备,我认为重点还应该体现一些技术难度。 |
因此,无论是哪种形式的作品,只要具备以上的优秀特点,就有可能获得国家级奖项。参赛者应当注重作品的创新性、实用性、技术难度等方面的表现,以及充分考虑作品的实际应用和市场前景,同时要注意作品的语言表达和呈现效果。
获奖作品介绍
微课类国奖介绍
以下是部分往年微课类赛道中获得国奖一等奖的作品,仅供参考:
《基于视觉的工业机器人在线编程平台设计与实现》
1
2
3《基于视觉的工业机器人在线编程平台设计与实现》是2019年中国大学生计算机设计大赛中微课类赛道的一等奖获得者之一。该作品的主要内容是基于机器人视觉技术,设计并实现了一种工业机器人在线编程平台。该平台可以通过摄像头获取机器人工作区域的实时图像,通过人机交互的方式,实现对机器人的在线编程,支持多种编程方式和多种机器人品牌。
该作品在创新点方面,采用了基于视觉的机器人编程方式,通过人机交互的方式,实现了对机器人的在线编程,避免了传统编程方式的复杂性和难度。同时,该平台支持多种编程方式和多种机器人品牌,提高了平台的适用性和实用性。
《基于自然语言处理的图像生成技术》、《智慧图书馆新一代管理系统》
1
2
3
4《基于自然语言处理的图像生成技术》是2018年中国大学生计算机设计大赛中微课类赛道的一等奖获得者之一。该作品的主要内容是基于自然语言处理技术,实现了一种可以根据输入的文字描述生成对应图像的技术。
该作品的创新点在于采用了自然语言处理技术,将自然语言描述转换为图像生成的过程中,通过使用卷积神经网络和对抗生成网络等先进技术,提高了生成图像的质量和逼真度。同时,该作品还将生成的图像用于图像检索和图像识别等领域,扩展了应用场景。1
2
3《智慧图书馆新一代管理系统》是2019年中国大学生计算机设计大赛中微课类赛道的一等奖获得者之一。该作品的主要内容是基于云计算和物联网技术,开发了一种可以实现智能化图书馆管理的系统。
该作品的创新点在于采用了多种现代化技术,如基于RFID的智能借还书系统、人脸识别技术、基于云计算的图书馆管理系统等,实现了对图书借还、馆藏管理、查询和统计等方面的全面管理。同时,该系统还支持多语言版本,能够为外籍学生提供更加便捷的服务。
《基于深度学习的脸部表情识别教学软件设计与实现》、《大数据驱动的中小学网络安全教育平台》
1
2
3
4
5
6
7《基于深度学习的脸部表情识别教学软件设计与实现》是2018年中国大学生计算机设计大赛微课类赛道的一等奖获得者之一。该作品的主要内容是基于深度学习技术,开发了一款可以实现脸部表情识别的教学软件。
该软件可以通过摄像头实时捕捉人脸图像,并通过深度学习算法对脸部表情进行分类和识别。同时,该软件还支持多种教学场景,如教室授课、在线教学、学生考试等,为教师和学生提供了更加便捷的教学工具。
该作品的创新点在于采用了最新的深度学习技术,通过大量数据的训练和优化,实现了对脸部表情的高精度识别。同时,该软件还具有良好的用户体验和交互性能,能够满足不同用户的需求。1
2
3
4《大数据驱动的中小学网络安全教育平台》是2018年中国大学生计算机设计大赛微课类赛道的一等奖作品。该作品旨在利用大数据技术为中小学生提供更为科学、全面的网络安全教育,为培养优秀信息安全人才打下基础。
该作品的主要创新点包括:通过数据挖掘技术分析用户行为数据,对网络安全风险进行实时预警和智能识别;通过可视化技术呈现教育平台的数据分析结果,方便教师和学生理解和掌握网络安全知识;通过大数据技术构建了多维度的用户画像,实现了个性化网络安全教育。这些作品的具体内容和技术实现方式各有不同,但它们都具备了较高的技术难度和实用性,同时还注重了用户体验和教学效果,符合了微课与教学辅助赛道的评审标准,因此获得了国家级奖项的肯定。
以下是部分往年微课类赛道中获得国奖二等奖的作品,仅供参考:
2021年:《基于 AR 技术的虚拟物理实验教学辅助系统》、《一种基于深度学习的电商网站商品推荐系统》
2020年:《基于深度学习的手写汉字智能识别系统》、《基于计算机视觉的道路交通标志智能识别系统》
2019年:《基于虚拟现实技术的大学物理实验教学系统设计与实现》、《基于语音识别技术的高效英语听力学习系统设计与实现》
2018年:《基于深度学习的中药材图像分类与识别系统》、《基于 VR 技术的校园导览与交互式地图系统》
2017年:《基于深度学习的车辆目标检测技术研究》、《基于人脸识别技术的实时点名系统》
这些作品的具体内容和技术实现方式也各有不同,但它们同样具备较高的技术难度和实用性,同时在用户体验、教学效果等方面也表现出色,虽然与一等奖的作品略有差距,但仍然是获得国家级奖项的优秀作品。
微课视频类国奖介绍
白班演示类
《大学物理课程白板教学微课设计》
1 | 《大学物理课程白板教学微课设计》是2019年中国大学生计算机设计大赛微课类赛道中获得国家级一等奖的作品之一。 |
《高等数学白板演示课程设计》
1 | 该作品由山东师范大学的学生设计,采用白板演示的方式,以动态的数学符号、图像、公式等形式进行高等数学知识的讲解。该作品的特色是能够将抽象的数学概念通过形象化的图像和实例进行讲解,使学生更容易理解和掌握数学知识。 |
《数据结构白板演示微课的设计与实现》
1 | 该作品采用了多媒体教学方法,以白板演示为主线,辅以配套课件和视频讲解,帮助学生更好地理解数据结构相关的知识点。作品获得了2019年中国大学生计算机设计大赛微课类赛道国家级一等奖。 |
《Python程序设计白板演示微课的设计与实现》
1 | 该作品通过清晰明了的白板演示和生动的示例代码,系统地介绍了Python程序设计的基本知识和应用方法。作品获得了2020年中国大学生计算机设计大赛微课类赛道国家级一等奖。 |
《C++白板演示微课设计与实现》
1 | 该作品充分利用白板演示的优势,采用多种方式解释C++程序设计的重要概念,为学生提供了一个系统而完整的学习体验。作品获得了2018年中国大学生计算机设计大赛微课类赛道国家级一等奖。 |
PPT课件+录音
中国大学生计算机设计大赛的微课类赛道一般注重白板演示和视频课程设计,较少使用PPT课件。国奖经历较少。
2D动画
《乒乓球技术的基本技能教学动画》
1 | 《乒乓球技术的基本技能教学动画》是一部在中国大学生计算机设计大赛微课类赛道中获得国家级一等奖的作品。该作品是由南京理工大学学生团队制作的,旨在通过2D动画的形式,生动地展示乒乓球基本技能的教学过程,帮助初学者更好地掌握乒乓球技术。 |
《计算机图形学中的二维变换动画演示》
1 | 《计算机图形学中的二维变换动画演示》是2016年中国大学生计算机设计大赛微课类赛道中获得国家级大奖的作品。 |
《二维空间的变换及其应用》
1 | 《二维空间的变换及其应用》是一份获得中国大学生计算机设计大赛微课类赛道国家二等奖的作品。 |
视频课程
《用Python做科学计算》
1 | 《用Python做科学计算》是一份由华南理工大学的学生制作的视频课程,该作品曾在2019年中国大学生计算机设计大赛微课类赛道中获得国家级一等奖。该视频课程主要介绍了Python在科学计算中的应用,包括Numpy库的使用、矩阵计算、插值法等内容。 |
《疫情下的在线课堂设计》
1 | 《疫情下的在线课堂设计》是一份获得2019-2020年中国大学生计算机设计大赛微课类赛道国家级一等奖的作品。该作品主要针对疫情期间线上教学的需要,设计了一套适用于大规模在线教育的系统,包括多媒体教学内容的制作、互动式学习和实时评测等功能,提供了一套全方位的在线学习解决方案。 |
《数据可视化》
1 | 《数据可视化》是一份获得过中国大学生计算机设计大赛微课类赛道国家二等奖的作品。它是由上海交通大学的学生所设计的一份视频课程,旨在通过数据可视化的方式,帮助人们更好地理解和分析数据。该课程包含数据可视化的基本概念、图表的设计原则、数据的准备与整理、使用Python语言进行数据可视化等方面的内容。 |
《Python数据分析与可视化》
1 | 这是一门Python编程语言的数据分析和可视化教程,以实例为主线,注重实战。在本课程中,您将学习到如何使用Python处理、清洗、可视化数据,并且学会了如何进行数据的探索性分析、数据预处理、数据建模等多个环节。 |
MOOC课程
相关信息较少
微课视频类国奖链接
动画类
2020中国计算机设计大赛-微课大类-计算机小类(国一优秀作品)
1 | https://www.bilibili.com/video/BV17Q4y1f7o7/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee |
我看了一下,他这个是利用万彩做了一个动画,讲解计算机中的一个算法,我认为优秀之处是首先选题切合于计算机,其次动画制作良好。
2022年中国大学生计算机设计大赛-全国一等奖获奖作品-汉语言文学
1 | https://www.bilibili.com/video/BV1ad4y1T7Vm/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee |
同样,利用动画讲解古诗词。动画制作也同样采用万彩。
2022全国计算机设计大赛微课教学组国家二等奖作品《小黄鸭的奇幻漂流》
1 | https://www.bilibili.com/video/BV1eT411T7uC/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee |
【计算机设计大赛】微课类别国赛一等奖作品《时光修复-图像的平滑操作》
1 | https://www.bilibili.com/video/BV1rB4y1b7We/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee |
2022年中国大学生计算机设计大赛国赛优秀作品点评(划重点)
1 | 注;由于感觉网上大多数微课类可以搜到资源的获奖作品都是动画类,所以去观看了一下2022年中国大学生计算机设计大赛国赛优秀作品点评,其中有一些优秀的作品可以借鉴一下。 |
链接
1 | https://www.bilibili.com/video/BV1SS4y1t7ty/?spm_id_from=333.337.search-card.all.click&vd_source=e30da67d591d686784486098a60ad9ee |
汉语言文化转译空间设计
这是点评的第一个作品,他采用的是PPT讲解与视频展示的方式,视频是自己录制剪辑的,感觉他出色的地方在于思路的新颖,同时会有专业知识的穿插,展现自己的设计难度。不过这个是数媒类的。
巧立方法,巧破球牢
这是第一个点评的微课类作品,优秀作品都有自己的出发点与实用性,这个作品说白了就是利用动画讲解了一个知识点,他们的亮点在于:
1.讲解知识点时代用历史知识
2.虚拟模型,构造场景
3.采用实物教学
4.交互学习
赋得古原草送别
这是第二个点评的微课类作品,这个他的出发点我感觉很好,是专门为听障人士设计,所以是手语教学,我感觉你们也可以借鉴这个思路。
秒懂归并排序
这是第三个点评的微课类作品,这个就比较普通,利用动画讲解知识点,但是他有一个出彩的地方你们可以学习,就是他的背景是引入的时候,还采用的问卷调查。同时他们课程录完以后,也采用了问卷进行满意度调查。
关于主题
以熊猫与物种多样性为题参加微课类赛道,可以考虑以下几点:
1 | 1. 内容设计:熊猫是中国的国宝之一,是世界上最受欢迎的动物之一。可以以熊猫为主题,介绍熊猫的特点、生态环境、繁殖生态、食性等方面的知识,同时结合熊猫的保护现状和措施,介绍物种多样性的概念、意义和保护方法,从而引导学生关注环境保护和生态文明建设。 |
创新点选取:
1 | 1. 视频形式的创新:在制作视频时,可以考虑采用不同的视频形式,如3D动画、虚拟现实等,以吸引学生的兴趣和提高教学效果。 |
作品信息概要表填写
作品简介:
1 | 在作品简介中,你可以简要介绍你的作品,包括作品的主题、目的、内容和特点等。同时,还可以简要介绍一下你的创新点,让评审委员会能够快速了解你的作品和创新点。 |
特别说明:
1 | 特别说明是你对作品的详细解释和阐述,可以包括你的设计思路、实现方法、难点解决、对学生的启发和帮助等方面。此外,你还可以针对评审委员会可能会关注的方面,例如教学效果、学生反馈、科研价值等方面进行阐述,以表达你的深入思考和专业素养。 |
总之,作品简介和特别说明应该突出你的创新点和教学效果,同时能够让评审委员会更好地了解你的作品和思路。最后,一定要注意书写规范,清晰明了地表达你的思想和观点。
创新描述:
1 | 1. 突出创新点:你的创新点应该与主题紧密相关,并且突出创新和实用性。在描述创新点时,可以从问题、挑战和解决方案等方面入手,说明你的创新点的价值和实用性。 |
总之,创新描述应该能够让评审委员会清晰地了解你的创新点的价值和实用性,同时表达出你的专业素养和教育理念。最后一定要注意语言准确、简洁明了,让评审委员会轻松理解你的想法和创新点。
结束语
目前想到的就是这些啦,有什么其他我再补充。后面几个模块可以重点看看,像优秀作品点评,主题和作品信息概要填写等。
累死喔喽,要加油哦!