简历
技术技能
- 熟悉 Java 中 IO 流的分类,熟悉 Java 中集合分类以及 ConcurrentHashMap 的底层原理。
- 熟悉多线程和线程池,熟悉 Java 中锁的分类,如乐观锁和悲观锁。
- 了解常见的数据结构及算法,如数组、栈、队列、二叉树;冒泡 、插入、选择排序等。
- 了解 JVM 内存结构、类加载器和双亲委派机制等,了解常见的垃圾回收器和垃圾回收算法。
- 熟悉 MySQL 安装、查询、子查询、内外连接、索引的底层数据存储结构等。
- 熟悉事务的隔离级别和数据库锁,了解数据库优化和 sql 优化方案。
- 熟悉 Redis 的基本数据类型,主从复制,哨兵模式和集群。
- 熟悉 Sping + SpringMVC + Mybatis 开发框架,熟悉 Spring 的循环依赖,Bean 的生命周期等。
- 具备基于 Vue + SpringBoot 的前后端分离开发经验。
- 了解 SpringCloud 常用组件的使用,如 Nacos 的服务注册及配置管理、OpenFeign 的远程调用等。
- 熟悉 HTML、CSS 与 JavaScript,使用过 Ajax 进行前后端数据交互,了解 RESTful API 设计规范。
- 了解 Vue 组件、钩子函数等相关知识点,可以使用 Vue 完成基础的页面编写和前后端数据交互。
- 了解网络通信框架,了解强缓存和协商缓存,了解 TCP 建立连接和断开连接的过程。
- 熟悉 Linux 操作系统,掌握常见指令,如文件操作、权限设置等,具备基本的系统管理和维护能力。
- 熟练使用 Git、IDEA、ChatGPT、Swagger、Postman 等工具提高开发写作效率。
实习经历
南京攸达网络科技有限公司
2024/07 - 2024/09
工作内容:
负责系统从 CentOS 向 Euler 操作系统的迁移工作,完成数据库与应用服务的平稳过渡与性能检测。
技术要点:
- 将部署在 CentOS 系统上的 MySQL、Tomcat 及 Nginx ,完整迁移部署至 Euler 系统中。
- 编写 Shell 脚本实现对 CPU、内存占用的实时监控及数据库的定时备份。
- 结合 systemctl 与 Python 脚本实现应用的自动化重启与状态维护。
- 配置两台数据库服务器实现 MySQL 的互为主从,并结合 Nginx 配置双节点反向代理,实现异常切换。
项目经验
编程导航后台管理
开发环境:JDK8、Mysql5.7、Redis6.2.18、Node16.20.2
项目架构:Maven3.6.3、SpingBoot、Vue3
项目描述:编程导航后台管理(Programming Navigator Admin, PNA)是一种用来管理和控制编程社区的软件系统,帮助工作人员高效管理编程社区内用户、文章及评论。PNA 可以追踪和记录文章以及评论的发布情况,并对评论数进行实时可视化分析。
项目实现:
- 登录模块:验证码,管理员注册/登录/退出,管理员头像云存储,管理员信息修改。
- 用户模块:新增用户,用户的启动和禁用,查询和修改用户角色,重置用户密码。
- 文章/评论模块:新增和修改文章/评论,设置文章/评论状态,评论可视化分析。
技术要点:
- 使用 JWT 生成 token,实现登录鉴权,并使用双令牌策略刷新缓存。
- 使用 Redis 分布式缓存存取评论数据。
- 使用 RBAC 进行权限菜单树的开发,实现了用户和权限逻辑分离。
- 进行验证码的开发,使用 SHA256 对密码进行盐值加密。
- 使用动态数据源对数据进行分库存储,使用 Interceptor 进行登录拦截。
- 使用 AliyunOSS 存取管理员头像,使用 PageHelper 插件实现分页查询。
- 使用 AOP 进行统一日志和声明式事务管理。
- 设置 GlobalExceptionHandler 处理异常,并使用 Knife4j 文档进行后端接口测试。
- 使用 Vite 脚手架搭建 Vue 前端框架,并使用 ECharts 进行可视化分析。
源码地址:https://gitee.com/make-it-easy/programming-navigator-admin
技术技能
熟悉 Java 中 IO 流的分类,熟悉 Java 中集合分类以及 ConcurrentHashMap 的底层原理
ConcurrentHashMap
ConcurrentHashMap 是线程安全的,它在HashMap的基础上,通过 CAS
或者 synchronized
来保证线程安全的,并且缩小了锁的粒度,查询性能也更高。
JDK 1.7 ConcurrentHashMap采用的是分段锁,即每个Segment是独立的,可以并发访问不同的Segment ,默认是16个Segment ,所以最多有16个线程可以并发执行。 而JDK 1.8移除了Segment ,缩小了锁的粒度,锁只在链表或红黑树的节点级别上进行。通过CAS进行插入操作,只有在更新链表或红黑树时才使用synchronized,并且只锁住链表或树的头节点,进一步减少了锁的竞争,并发度大大增加。
JDK 1.7 ConcurrentHashMap只使用了数组+链表的结构,而JDK 1.8和HashMap一样引入了红黑树。另外ConcurrentHashMap
在Java 8及之后版本中引入了ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
构造方法,允许设置初始容量、负载因子和并发级别。
注意:ConcurrentHashMap中key不能为空
Object类中有哪些常见的方法
- toString()
- equals(Object obj)
- hashCode()
- wait()
- notify()
熟悉多线程和线程池,熟悉 Java 中锁的分类,如乐观锁和悲观锁
Java中有哪些锁
口诀:克功悲独户,自请分
可重入锁,公平锁,悲观锁,独享锁,互斥锁,自旋锁,轻量级锁,分段锁。
- 公平锁/非公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程不按照申请锁的顺序来获取锁,有可能会造成饥饿现象。
Java ReentrantLock
和 Synchronized
是非公平锁。
- 可重入锁(递归锁):在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
Java ReentrantLock
和 Synchronized
是可重入锁。
- 独享锁/共享锁:独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
Java ReentrantLock
和 Synchronized
是独享锁。但是对于Lock的另一个实现类 ReadWriteLock
,其读锁是共享锁,其写锁是独享锁。
- 互斥锁/读写锁:上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是
ReentrantLock
。读写锁在Java中的具体实现就是 ReadWriteLock
- 乐观锁/悲观锁:乐观锁认为多线程同时修改共享资源的概率比较低,所以先共享资源,如果出现同时修改的情况,再放弃本次操作。悲观锁认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁。(注意,乐观锁不加锁)
- 分段锁:分段锁其实是一种锁的设计,并不是具体的一种锁,对于
ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
- 偏向锁/轻量级锁/重量级锁:这三种锁是指锁的状态,并且是针对
Synchronized
。Java 5时引入,这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
- 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询(一直申请访问),直到锁被释放。
乐观锁
乐观锁的原理主要基于版本号或时间戳来实现。在每次更新数据时,先获取当前数据的版本号或时间戳,然后在更新时比对版本号或时间戳是否一致,若一致则更新成功,否则表示数据已被其他线程修改,更新失败。**CAS(Compare-And-Swap)
是实现乐观锁的核心算法。**
悲观锁
在Java中,常见的悲观锁实现是使用synchronized
关键字或ReentrantLock
类。这些锁能够确保同一时刻只有一个线程可以访问被锁定的代码块或资源,其他线程必须等待锁释放后才能继续执行。
了解常见的数据结构及算法,如数组、栈、队列、二叉树;冒泡 、插入、选择排序等
数据结构
字符和字符串
字符
- legnth() - 字符的长度
字符串
- legnth - 字符串的长度
- s.charAt(i) - 获取字符串s中的第i个字符
- str.trim() - 删除字符串首尾空格
- substring(int beginIndex, int endIndex) - 截取 [beginIndex, endIndex) 区间的子字符串,左闭右开
数组
数组的特点
- 数组的长度不能改变
- 数组中的元素的类型必须是同一种数据类型(基本类型/引用类型)
- 存入数组中的元素,按照存入的顺序排列
背诵技巧:元长存
数组的动态创建方式和静态创建方式的区别
动态方式:只指定长度,由系统给出初始化值
1
| int arr2[] = new int[5];
|
静态方式:给出初始化值,由系统指定长度
1
| int[] arr1 = new int[]{1, 3, 5};
|
常用方法
基本操作(重要)
- push() - 在数组末尾添加一个或多个元素
- pop() - 移除并返回数组的最后一个元素
- concat() - 合并两个或多个数组,返回新数组
修改和访问方法
- indexOf() - 返回元素在数组中首次出现的位置
- lastIndexOf() - 返回元素在数组中最后一次出现的位置
- includes() - 判断数组是否包含某个元素
迭代方法
- forEach() - 对数组每个元素执行函数
- filter() - 返回满足条件的元素组成的新数组
- find() - 返回第一个满足条件的元素
- findIndex() - 返回第一个满足条件的元素的索引
栈和队列
特点
栈是后进先出(LIFO)结构;队列是先进先出(FIFO)结构。
常用方法
栈(重要)
- push() - 将元素压入栈顶
- pop() - 移除并返回栈顶元素
- peek()/top() - 查看栈顶元素但不移除
队列(Qeque)(重要)
- enqueue() - 向队尾添加元素
- dequeue() - 移除并返回队首元素
- peek()/front() - 查看队首元素但不移除
公共方法(重要)
- isEmpty() - 检查栈/队列是否为空
- size() - 返回栈/队列中元素数量
- clear() - 清空栈/队列
双端队列(Deque)
- addFront() - 在队首添加元素
- addRear() - 在队尾添加元素
- removeFront() - 移除队首元素
- removeRear() - 移除队尾元素
- peekFront() - 查看队首元素
- peekRear() - 查看队尾元素
二叉树
前、中、后序遍历方式
前序:根左右,在节点的左侧画点
中序:左根右,在节点的下侧画点
后序:左右根,在节点的右侧画点
前序遍历
中序遍历
后序遍历
算法
排序算法的特性
自适应排序:数据的输入情况会影响排序算法的时间复杂度,非自适应指的是数据的输入情况不会影响排序算法的时间复杂度
稳定排序:根据元素某一数值进行排序的排序算法不会改变元素之间的相对位置
原地排序:不需要额外空间
排序算法共性(人为)
遵从自小到大排序
选择排序
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
| package practiceClass.a0421;
public class a01_test { void selectionSort(int[] nums) { int n = nums.length; for (int i = 0; i < n - 1; i++) { int minIndex = i; for (int j = i + 1; j < n; j++) { if (nums[j] < nums[minIndex]) minIndex = j; } int temp = nums[i]; nums[i] = nums[minIndex]; nums[minIndex] = temp; } }
public static void main(String[] args) { a01_test sorter = new a01_test(); int[] nums = {1, 2, 5, 6, 7, 2}; sorter.selectionSort(nums);
System.out.println("Sorted Array:"); for (int num : nums) { System.out.print(num + " "); } System.out.println(); } }
|
冒泡排序
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
| package practiceClass.a0421;
public class a02_test { void bubbleSort(int[] nums) { int n = nums.length; for (int i = n - 1; i > 0; i--) { boolean flag = false; for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; flag = true; } } if (!flag) break; } }
public static void main(String[] args) { a02_test sorter = new a02_test(); int[] nums = {1, 2, 5, 6, 7, 2};
sorter.bubbleSort(nums);
System.out.println("Sorted Array:"); for (int num : nums) { System.out.print(num + " "); } System.out.println(); } }
|
插入排序
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
| package practiceClass.a0421;
public class a03_test { void insertionSort(int[] nums){ int n = nums.length; for (int i = 1; i < n; i++) { int base = nums[i]; int j = i - 1; while (j >= 0 && nums[j] > base){ nums[j + 1] = nums[j]; j--; } nums[j + 1] = base; } }
public static void main(String[] args) { a03_test sorter = new a03_test();
int[] nums = {1, 2, 5, 6, 7, 2};
sorter.insertionSort(nums);
System.out.println("Sorted Array:"); for (int num : nums) { System.out.print(num + " "); } System.out.println(); } }
|
了解 JVM 内存结构、类加载器和双亲委派机制等,了解常见的垃圾回收器和垃圾回收算法
JVM内存相关
熟悉 MySQL 安装、查询、子查询、内外连接、索引的底层数据存储结构等
数据库相关
熟悉事务的隔离级别和数据库锁,了解数据库优化和 sql 优化方案
数据库优化
采用动态数据源或者分库分表
数据库相关
熟悉 Redis 的基本数据类型,主从复制,哨兵模式和集群
Redis配置
Redis基础
主从
一主多从
最低: 1 写 3 读
主从,(高可用) 确保 写 宕机的时候 ,其中一个读 上线状态转移成 写
为什么要3个, 因为只有2个没有办法做选举 ,造成 脑裂
修改参数
通用
1 2 3 4
| port 7001 ~ 7004 pidfile logfile dir
|
非通用参数
1 2
| # replicaof <masterip> <masterport> replicaof 127.0.0.1 7002 ~ 7004
|
哨兵
通过哨兵(工具 )实现主从,而无需自己手动选举
- 监控 master 什么时候宕机
- 故障转移 当 master 宕机的时候,设置新的 master
配置
从解压的 redis 包中找到 sentinel.conf
1 2 3 4 5 6
| daemonize yes pidfile "/bigdata/sentinel/redis-sentinel.pid" logfile "/bigdata/sentinel/redis-sentinel.log" sentinel monitor mymaster 127.0.0.1 7001 1 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000
|
启动
1
| master/bin/redis-sentinel sentinel.conf
|
集群
cluster
3主 3 从 (最低三从),必须是奇数个服务器 ,防止脑裂
配置
配置文件要修改的
1 2 3 4
| port 8001-8006 pidfile logfile dir
|
集群
1 2
| cluster-enabled yes cluster-config-file nodes(port).conf
|
每个文件都要修改6项内容
启动
建立集群同时指定 集群的 master 和 slave
1 2 3 4 5 6 7 8
| redis1/bin/redis-cli --cluster create \ 127.0.0.1:8001 \ 127.0.0.1:8002 \ 127.0.0.1:8003 \ 127.0.0.1:8004 \ 127.0.0.1:8005 \ 127.0.0.1:8006 \ --cluster-replicas 1
|
启动的时候 yes
默认前三个是主,后三个是从
熟悉 Sping + SpringMVC + Mybatis 开发框架,熟悉 Spring 的循环依赖,Bean 的生命周期等
AOP
概述
面向切面编程(Aspect Oriented Programming),它利用一种称为”横切”的技术,剖解开封装的对象内部,将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为切面,降低模块之间的耦合度,有利于未来的操作和维护。
Spring
中**AOP
代理由 Spring 的IOC
容器负责生成、管理**,其依赖关系也由 IOC 容器负责管理。因此, AOP 代理可以直接使用容器中的其它 bean 实例作为目标。Spring 创建代理的规则为:
- 默认使用 JDK 动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了
- 当需要代理类,而不是代理接口的时候,Spring 会切换为使用 CGLIB代理 ,也可强制使用 CGLIB
纵观 AOP 编程,程序员只需要参与三个部分:
- 定义普通业务组件
- 定义切入点,一个切入点可能横切多个业务组件
- 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作
Spring中AOP的通知类型
- @Around(“pointCut()”) :环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before(“pointCut()”):前置通知,此注解标注的通知方法在目标方法前被执行
- @After(“pointCut()”):后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning(“pointCut()”): 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing(“pointCut()”): 异常后通知,此注解标注的通知方法发生异常后执行
IOC
什么是 IOC
Spring的IOC(inverse of contorl),也就是控制反转,它的核心思想是让对象的创建和依赖关系由容器来控制,不是我们自己new出来的,这样各个组件之间就能保持松散的耦合。IOC是通过依赖注入(DI)来实现的。
为什么需要存在一个容器?
解耦,将对象之间的相互依赖的关系交给IOC容器管理,并让容器完成对象注入,开发者不需要知道一个Service依赖的其 他类,只需要从容器中拿取即可。
Spring Bean
Bean : 被IOC容器所管理的对象俗称Bean,Bean可以通过XML、注解、配置类进行定义。 一般采用注解,如下:
- @Component:通用注解
- @Repository:持久层 DAO 层
- @Service:服务层 Service
- @Controller: Spring MVC控制层
怎么注入 Bean?
通常采用注解
- @AutoWired按类型装配
Spring的循环依赖
什么是循环依赖
两个或者两个以上的 bean
互相持有对方,最终形成闭环。比如 Bean A
依赖于 Bean B
,而 Bean B
又依赖于 Bean A
,形成了一个循环依赖关系。这种情况下,如果不处理,会导致 Spring
容器无法完成 Bean
的初始化,从而抛出循环依赖异常。
怎么检测是否存在循环依赖
检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明产生循环依赖了。
如何解决
构造器循环依赖:Spring容器在创建Bean时,如果遇到循环依赖,通常是无法处理的,因为这会导致无限递归创建Bean实例。所以,构造器注入是不支持循环依赖的。
字段注入或Setter注入:使用了三级缓存来解决循环依赖问题。
- 首先,Spring容器会创建一个Bean的原始实例,但此时Bean的属性尚未设置,这个实例被存放在一级缓存中。
- 当Bean的属性被设置时,如果属性值是其他Bean的引用,Spring会去检查二级缓存,看是否已经有该Bean的引用存在。
- 如果二级缓存中没有,Spring会尝试创建这个被引用的Bean,并将其放入三级缓存。
- 最后,当Bean的属性设置完成后,原始的Bean实例会被放入二级缓存,供其他Bean引用
使用@Lazy
注解:通过@Lazy
注解,可以延迟Bean的加载,直到它被实际使用时才创建,这可以避免一些循环依赖的问题。
Bean的生命周期
背诵技巧:通天(填)A(Aware),B始(使)毁
Spring Bean的生命周期,其实就是Spring容器从创建Bean到销毁Bean的整个过程。这里面有几个关键步骤:
- 实例化Bean: Spring容器通过构造器或工厂方法(推荐)创建Bean实例。
- 设置属性:容器会注入Bean的属性,这些属性可能是其他Bean的引用,也可能是简单的配置值。
- 检查Aware接口并设置相关依赖:如果Bean实现了
BeanNameAware
或BeanFactoryAware
接口,容器会调用相应的setBeanName
或setBeanFactory
方法。
BeanPostProcessor
的第一次调用:在Bean初始化之前,Spring会调用所有注册的BeanPostProcessor
的postProcessBeforeInitialization
方法。
- 初始化
Bean
: 如果Bean实现了InitializingBean
接口,容器会调用其afterPropertiesSet
方法。同时,如果Bean定义了init-method
,容器也会调用这个方法。
BeanPostProcessor
的第二次调用:在Bean初始化之后,容器会再次调用所有注册的BeanPostProcessor
的postProcessAfterInitialization
方法。
- 使用Bean:此时,Bean已经准备好了,可以被应用程序使用了。
- 处理
DisposableBean
和destroy-method
:当容器关闭时,如果Bean实现了DisposableBean
接口或者定义了destroy-method
,容器会调用其destroy
方法。
- Bean销毁:最后,Bean被Spring容器销毁,结束了它的生命周期。
具备基于 Vue + SpringBoot 的前后端分离开发经验
了解 SpringCloud 常用组件的使用,如 Nacos 的服务注册及配置管理、OpenFeign 的远程调用等
Nacos 的服务注册及配置管理
Nacos (Naming and Configuration Service)是阿里巴巴开源的一个服务注册发现、配置管理的平台,主要用于帮助构建和管理微服务架构。
核心功能
- 服务注册与发现:微服务启动时将自己的信息(如IP、端口)注册到Nacos,其他服务可通过Nacos查询可用服务实例。
- 动态配置管理:集中管理配置信息(如数据库连接参数) ,支持实时更新配置而无需重启服务(热启动)。
- 服务健康监测:通过心跳机制监控服务状态,自动剔除故障实例,保证服务可用性。
- 流量管理:支持基于权重的路由策略,实现负载均衡和灰度发布。
背诵技巧:浮动伏流
需要与Nacos交互的服务
在典型微服务项目中,以下服务需要与Nacos交互:
- 服务提供者(Provider) 注册服务:启动时向Nacos注册自己的服务名称、IP和端口。 维持心跳:定期发送心跳包,证明自己处于健康状态。若停止发送,Nacos会将其标记为不可用。
- **服务消费者(Consumer) 服务发现:**从Nacos获取目标服务的实例列表,并通过负载均衡策略(如轮询、随机)调用服务。 订阅配置:例如读取数据库连接信息或功能开关,配置变更时自动生效。
- 配置管理客户端所有需要动态配置的服务(如Spring Boot应用)会连接Nacos,监听配置变化。例如,修改日志级别后,服务无需重启即可生效。
- 网关服务(如Spring Cloud Gateway) 动态路由:从Nacos获取路由规则,例如将“/user/**”的请求转发到用户服务。 服务实例列表:实时更新后端服务的可用实例,避免将请求发送到已宕机的节点。
服务注册流程
接下来,我们把item-service
注册到Nacos,步骤如下:
添加依赖
在item-service
的pom.xml
中添加依赖:
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
配置Nacos
在item-service
的application.yml
中添加nacos地址配置:
1 2 3 4 5 6
| spring: application: name: item-service cloud: nacos: server-addr: 192.168.150.101:8848
|
启动服务实例
为了测试一个服务多个实例的情况,我们再配置一个item-service
的部署实例:
然后配置启动项,注意重命名并且配置新的端口,避免冲突:
重启item-service
的两个实例:
访问nacos控制台,可以发现服务注册成功:
点击详情,可以查看到item-service
服务的两个实例信息:
服务发现流程
spring-cloud-starter-alibaba-nacos-discovery 依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
服务发现除了要引入nacos依赖以外,由于还需要负载均衡,因此要引入SpringCloud提供的LoadBalancer依赖。
OpenFeign 的远程调用
需求
OpenFeign 是一个声明式的 Web 服务客户端,它通过注解方式简化了 HTTP 客户端的编写,让我们只需定义接口而无需手动实现 HTTP 请求逻辑。
之前,利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了。因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到OpenFeign组件了。
远程调用的关键点就在于四个:
所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。
使用方式
引入依赖
在cart-service
服务的pom.xml中引入OpenFeign
的依赖和loadBalancer
依赖:
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
|
启用OpenFeign
接下来,我们在cart-service
的CartApplication
启动类上添加注解,启动OpenFeign功能:
编写OpenFeign客户端
在cart-service
中,定义一个新的接口,编写Feign客户端:
其中代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.hmall.cart.client;
import com.hmall.cart.domain.dto.ItemDTO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("item-service") public interface ItemClient {
@GetMapping("/items") List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); }
|
这里只需要声明接口,无需实现方法。接口中的几个关键信息:
@FeignClient("item-service")
:声明服务名称
@GetMapping
:声明请求方式
@GetMapping("/items")
:声明请求路径
@RequestParam("ids") Collection<Long> ids
:声明请求参数
List<ItemDTO>
:返回值类型
有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items
发送一个GET
请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>
。
我们只需要直接调用这个方法,即可实现远程调用了。
使用FeignClient
最后,我们在cart-service
的com.hmall.cart.service.impl.CartServiceImpl
中改造代码,直接调用ItemClient
的方法:
feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作。
而且,这里我们不再需要RestTemplate了,还省去了RestTemplate的注册。
熟悉 HTML、CSS 与 JavaScript,使用过 Ajax 进行前后端数据交互,了解 RESTful API 设计规范
type取值 |
描述 |
text |
文本框 |
password |
密码框 |
radio |
单选框 |
checkbox |
复选框 |
file |
上传图像 |
hidden |
隐藏字段 |
submit |
提交按钮 |
reset |
重置按钮 |
button |
普通按钮 |
image |
图片按钮 |
背诵技巧:文秘(密)担(单)负(复)上瘾(隐)重提谱(普)图
CSS中选择器的分类
基础选择器
- 标签选择器
- 类选择器
- id选择器
- 通配符选择器
关系选择器
- 交集选择器:没有符号
- 并集选择器:以逗号隔开
- 亲子选择器:以>隔开
- 后代选择器:以空格隔开
属性选择器
两种情况,一种是方括号中属性名+属性值,一种是方括号中只有属性名
伪类选择器
伪类通过冒号来定义元素的状态
背诵技巧:机(基)关属委(伪)
不算技巧的技巧:标类i通,交并亲后
JS中的基础数据类型
- number类型:整数、浮点数、NaN、infinity(正负无穷大)
- boolean类型:true 、false
- string类型:有三种表示形式,单引号、双引号、反引号,在js中没有字符的概念
- null类型
- undefined类型:既是一个数据类型,也是一个值;undefined作用是用来提醒我们该变量没有赋值就被使用了
背诵技巧:对应java中常量的分类来背。
不算技巧的技巧:nb s nu
JS中==和===的区别
- ==:只比较值,不比较类型
- ===:既比较值,也比较类型
JS中数组和Java中数组的区别
- js中数组的长度是可变的,而java中的数组长度不能改变
- js中数组可以存放多种类型的数据,而java中的数组只能存放同一种类型的元素
背诵技巧:常(长)数
ES6新特性有哪些
- let关键字,防止变量被重复声明,定义的变量具有块级作用域,必须先声明再使用
- const关键字,定义常量,声明时就要赋值,赋值一次之后,不能再次赋值
- 字符串支持反引号
- 箭头函数(java中的lambda)+ 可变参数
背诵技巧:class —- c(const)l(let)a(arrow)s(string)
JS中innerHtml和innerText的区别
- innertext:只能获取文本信息,不能获取标签信息
- innerhtml:既可以获取文本信息,也能获取标签信息
Ajax
概念
Ajax即Asynchronous Javascript And XML(异步JavaScript和XML)在 2005年被Jesse James Garrett提出的新术语,用来描述一种使用现有技术集合的‘新’方法,包括: HTML 或 XHTML, CSS, JavaScript, DOM, XML, XSLT, 以及最重要的XMLHttpRequest。
使用Ajax技术,网页应用能够快速地将增量更新呈现在用户界面上,而不需要重载(刷新)整个页面,这使得程序能够更快地回应用户的操作。
Ajax的实现方式
原生Ajax
第一步:创建XMLHttpRequest对象,用于和服务器交换数据,也是原生Ajax请求的核心对象,提供了各种方法。代码如下:
1 2
| var xmlHttpRequest = new XMLHttpRequest();
|
第二步:调用对象的open()方法设置请求的参数信息,例如请求地址,请求方式。然后调用send()方法向服务器发送请求,代码如下:
1 2 3
| xmlHttpRequest.open('GET','http://yapi.smart-xwork.cn/mock/169327/emp/list'); xmlHttpRequest.send();
|
第三步:我们通过绑定事件的方式,来获取服务器响应的数据。
1 2 3 4 5 6 7
| xmlHttpRequest.onreadystatechange = function(){ if(xmlHttpRequest.readyState == 4 && xmlHttpRequest.status == 200){ document.getElementById('div1').innerHTML = xmlHttpRequest.responseText; } }
|
Axios
Axios是对原生的AJAX进行封装,简化书写
使用方式
Axios的使用比较简单,主要分为2步:
引入Axios文件
1
| <script src="js/axios-0.18.0.js"></script>
|
使用Axios发送请求,并获取响应结果,官方提供的api很多,此处给出2种,如下
发送 get 请求
1 2 3 4 5 6
| axios({ method:"get", url:"http://localhost:8080/ajax-demo1/aJAXDemo1?username=zhangsan" }).then(function (resp){ alert(resp.data); })
|
发送 post 请求
1 2 3 4 5 6 7
| axios({ method:"post", url:"http://localhost:8080/ajax-demo1/aJAXDemo1", data:"username=zhangsan" }).then(function (resp){ alert(resp.data); });
|
axios()是用来发送异步请求的,小括号中使用 js的JSON对象传递请求相关的参数:
- method属性:用来设置请求方式的。取值为 get 或者 post。
- url属性:用来书写请求的资源路径。如果是 get 请求,需要将请求参数拼接到路径的后面,格式为: url?参数名=参数值&参数名2=参数值2。
- data属性:作为请求体被发送的数据。也就是说如果是 post 请求的话,数据需要作为 data 属性的值。
then() 需要传递一个匿名函数。我们将 then()中传递的匿名函数称为 回调函数,意思是该匿名函数在发送请求时不会被调用,而是在成功响应后调用的函数。而该回调函数中的 resp 参数是对响应的数据进行封装的对象,通过 resp.data 可以获取到响应的数据。
Restful风格代码
Restful风格的接口是一种基于资源的设计风格,用于构建面向Web的API。 REST (Representational State Transfer)是一种无状态的架构风格,它以 HTTP 协议为基础,通过定义资源和标准的操作方法来组织接口,使得客户端和服务器之间的交互更加简单、清晰和高效。
实际上Restful就是要求我们不要在URL上表现出动作,而是用HTTP动词代表动作,URL上只做资源。比如获取一个 user。
非Restful : GET /getUserByld?userld=1
Restful : GET /users/1
对应到SpringMVC代码就是 urlaa不写什么get之类的动词,而是通过HTTP请求(如GET,POST,PUT,DELETE)访问服务端资源。
了解 Vue 组件、钩子函数等相关知识点,可以使用 Vue 完成基础的页面编写和前后端数据交互
Vue指令
总括

- template相当于html
- script相当于javascript
- style相当于css
背诵技巧:A组爽(双)文是(事)薯(属)条
v-bind
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
| <template> <div> <!-- 动态图片路径 --> <img :src="user.avatar" :alt="user.name"> <!-- 动态类名 --> <button :class="['btn', { 'btn-active': isActive }]">Click me</button> <!-- 动态样式 --> <div :style="{ backgroundColor: bgColor, width: width + 'px', height: height + 'px' }"></div> <!-- 传递props给子组件 --> <user-card :user="currentUser" :is-following="followStatus"></user-card> <!-- 动态表单绑定 --> <input :value="inputValue" @input="inputValue = $event.target.value"> </div> </template>
<script> export default { data() { return { user: { name: 'John Doe', avatar: '/path/to/avatar.jpg' }, isActive: true, bgColor: '#42b983', width: 100, height: 100, currentUser: { /* ... */ }, followStatus: false, inputValue: '' } } } </script>
|
文本插值
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
| <template> <div> <!-- 1. 显示简单数据 --> <h1>{{ title }}</h1> <!-- 2. 显示计算后的数据 --> <p>总价: ¥{{ quantity * price }}</p> <!-- 3. 显示格式化后的日期 --> <p>当前时间: {{ formatDate(currentDate) }}</p> <!-- 4. 显示计算属性 --> <p>反转消息: {{ reversedMessage }}</p> <!-- 5. 条件显示 --> <div v-if="isLoggedIn"> 欢迎回来, {{ user.name }}! 您的会员等级是: {{ user.level.toUpperCase() }} </div> <!-- 6. 在属性中使用(不推荐,应使用v-bind) --> <span title="{{ tooltip }}">悬停查看提示</span> <!-- 7. 显示数组或对象内容 --> <ul> <li v-for="item in items" :key="item.id"> {{ item.name }} - 库存: {{ item.stock }} </li> </ul> </div> </template>
<script> export default { data() { return { title: '产品详情', quantity: 5, price: 29.9, currentDate: new Date(), message: 'Hello Vue!', isLoggedIn: true, user: { name: '张三', level: 'gold' }, tooltip: '这是一个提示信息', items: [ { id: 1, name: '商品A', stock: 10 }, { id: 2, name: '商品B', stock: 5 } ] } }, computed: { reversedMessage() { return this.message.split('').reverse().join('') } }, methods: { formatDate(date) { return date.toLocaleDateString() } } } </script>
|
指令集
指令 |
作用 |
v-bind(:) |
为HTML标签绑定属性值,如设置 href , css样式等 |
v-model |
在表单元素上创建双向数据绑定 |
v-on(@) |
为HTML标签绑定事件 |
v-if |
条件性的渲染某元素,判定为true时渲染,否则不渲染 |
v-else |
|
v-else-if |
|
v-show |
根据条件展示某元素,区别在于切换的是display属性的值 |
v-for |
列表渲染,遍历容器的元素或者对象的属性 |
v-slot(#) |
插槽 |
Vue钩子函数
- computed:
- created ()
- mounted ()
- methods:
Vue生命周期(钩子函数)
了解网络通信框架,了解强缓存和协商缓存,了解 TCP 建立连接和断开连接的过程
熟悉 Linux 操作系统,掌握常见指令,如文件操作、权限设置等,具备基本的系统管理和维护能力
熟练使用 Git、IDEA、ChatGPT、Swagger、Postman 等工具提高开发写作效率
实习经历
完整迁移部署至 Euler 系统中
数据库的定时备份
编辑备份 shell 脚本
vim mysql_back.sh
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
| #!/bin/bash #指定连接数据库信息(用户名、密码、连接地址、端口、安装目录) DB_USER="root" DB_PWD="password" DB_IP="host" DB_PORT="3306" #是指mysqldump命令所在目录 DB_DIR="/usr/local/mysql" #获取系统当前时间并格式化为:20210729 BAK_DATE=`date +%Y%m%d` #指定备份文件保存的天数 BAK_DAY=7 #指定备份的数据库,可以指定多个中间用空格隔开,或者不指定则默认全部备份 BAK_DATABASES=("") #指定备份路径 BAK_PATH="/data/mysql_back" #创建备份目录 mkdir ${BAK_PATH}/$BAK_DATE #开始执行备份 echo "------- $(date +%F_%T) Start MySQL database backup-------- " >>${BAK_PATH}/back.log #循环遍历 for database in "${BAK_DATABASES[@]}" do ${DB_DIR}/bin/mysqldump -u${DB_USER} -p${DB_PWD} --host=${DB_IP} --port=${DB_PORT} --databases $database > ${BAK_PATH}/${BAK_DATE}/${database}.sql done #创建压缩文件 cd ${BAK_PATH} tar -zcPf db_backup_${BAK_DATE}.tar.gz $BAK_DATE #删除备份目录 mv ${BAK_PATH}/$BAK_DATE /tmp1 #遍历备份目录下的文件 LIST=$(ls ${BAK_PATH}/db_backup_*) #获取截止时间,将早于改时间的文件删除 SECONDS=$(date -d "$(date +%F) - ${BAK_DAY} days" +%s) for index in ${LIST} do #获取文件名并格式化,获取时间,如20210729 timeString=$(echo ${index} | egrep -o "?[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]") if [ -n "$timeString" ] then indexDate=${timeString//./-} indexSecond=$( date -d ${indexDate} +%s ) #与当前时间做比较,把早于7天的文件删除 if [ $(( $SECOND - $indexDate )) -gt 0 ] then rm -f $index echo "-------deleted old file $index -------" >> ${BAK_PATH}/back.log fi fi done echo "-------$(date +%F_%T) Stop MySQL database backup-------- " >>${BAK_PATH}/back.log
|
查看日志文件
cat /opt/mysql_back/back.log
1 2 3
| cat /opt/mysql_back/back.log ------- 2021-07-29_18:25:09 Start MySQL database backup-------- -------2021-07-29_18:25:09 Stop MySQL database backup--------
|
解压压缩包查看备份脚本
1
| tar -zxvf db_backup_20210729.tar.gz
|
设置定时任务以实现定时备份
crontab -e
1 2 3
| crontab -e
0 3 * * * sh /root/mysql_back.sh
|
自动化重启与状态维护
状态维护
介绍
MySQL 通常通过系统包管理器(yum)安装,所以会自动注册到 systemctl 服务管理中,但是如果是通过下载压缩包手动安装,就需要手动注册到 systemctl 服务管理中。
对 MySQL 和 Tomcat 都进行注册,以下只介绍 MySQL 的注册流程
创建 systemd 服务文件
1
| sudo nano /etc/systemd/system/mysqld.service
|
添加服务配置内容
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
| [Unit] Description=MySQL Community Server After=network.target After=syslog.target
[Service] Type=forking User=mysql Group=mysql
# 根据你的实际安装路径修改 Basedir=/usr/local/mysql Datadir=/usr/local/mysql/data
# 指定PID文件位置(需与my.cnf中配置一致) PIDFile=/usr/local/mysql/data/mysqld.pid
# 启动命令 ExecStart=/usr/local/mysql/bin/mysqld --daemonize --basedir=$Basedir --datadir=$Datadir --pid-file=$PIDFile
# 停止命令 ExecStop=/usr/local/mysql/bin/mysqladmin -uroot -p shutdown
# 重启命令 ExecRestart=/usr/local/mysql/bin/mysqladmin -uroot -p shutdown && /usr/local/mysql/bin/mysqld --daemonize --basedir=$Basedir --datadir=$Datadir --pid-file=$PIDFile
# 设置重启策略 Restart=on-failure RestartSec=5s
PrivateTmp=true
[Install] WantedBy=multi-user.target
|
设置权限并重载 systemd
1 2 3
| ssudo chmod 644 /etc/systemd/system/mysqld.service sudo systemctl daemon-reload
|
修改数据目录权限
1
| sudo chown -R mysql:mysql /usr/local/mysql/data
|
启动 MySQL 服务
1
| sudo systemctl start mysql
|
设置开机自启
1
| sudo systemctl enable mysqld
|
验证服务状态
1
| sudo systemctl status mysqld
|
自动化重启
介绍
使用python
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
| import pymysql import subprocess import time
MYSQL_CONFIG = { 'host': 'localhost', 'user': 'root', 'password': 'your_mysql_password', 'database': 'test_db', 'port': 3306 }
TEST_DATA = [ ("Alice", "alice@example.com"), ("Bob", "bob@example.com"), ("Charlie", "charlie@example.com") ]
def insert_test_data(): """向 MySQL 插入测试数据""" try: connection = pymysql.connect(**MYSQL_CONFIG) cursor = connection.cursor() sql = "INSERT INTO users (name, email) VALUES (%s, %s)" cursor.executemany(sql, TEST_DATA) connection.commit() print(f"成功插入 {len(TEST_DATA)} 条测试数据。") except Exception as e: print(f"插入数据失败: {e}") finally: if connection: connection.close()
def restart_mysql(): """重启 MySQL 服务(Linux 系统)""" try: subprocess.run(["sudo", "systemctl", "restart", "mysql"], check=True) print("MySQL 重启成功,等待 5 秒...") time.sleep(5) except subprocess.CalledProcessError as e: print(f"重启 MySQL 失败: {e}")
def restart_tomcat(): """重启 Tomcat 服务(假设 Tomcat 安装在 /opt/tomcat)""" try: subprocess.run(["/opt/tomcat/bin/shutdown.sh"], check=True) time.sleep(3) subprocess.run(["/opt/tomcat/bin/startup.sh"], check=True) print("Tomcat 重启成功,等待 10 秒...") time.sleep(10) except Exception as e: print(f"重启 Tomcat 失败: {e}")
if __name__ == "__main__": insert_test_data() restart_mysql() restart_tomcat() print("流程执行完毕!")
|
互为主从
Mysql双机和Nginx反向代理
项目经验
使用 JWT 生成 token,实现登录鉴权,并使用双令牌策略刷新缓存
JWT
JWT介绍
JWT全称:JSON Web Token,定义了一种简洁的、自包含的格式,用于在通信双方间以 json 数据格式安全的传输信息。
简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。
自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。
JWT Token通常由三个部分组成,它们之间用“.” 分隔:
- Header (头部) :用于存储有关如何计算JWT签名的信息,如对象类型(type,通常为JWT) 、签名算法(alg,如HS256、 RSA等)等。这些信息会经过Base64Url编码后形成JWT的第一部分。
- Payload (有效载荷) :也称为”body” ,用于存储用户信息,如用户ID 、Email、角色(role)、权限信息(permission) 、签发时间 、过期时间(exp)等。这些信息同样会经过Base64Url编码后形成JWT的第二部分。
- Signature (签名) :使用Header中指定的算法,将编码后的Header、Payload以及一个密钥(secret)进行加密,生成最终的签名。这个签名用于验证信息在传输过程中是否被篡改,并且在使用私钥签名令牌的情况下,它还可以验证JWT的发送者是否正确。签名会作为JWT的第三部分。
JWT Token的应用
场景:登录认证。
- 登录成功后,生成令牌
- 后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
具体使用
1 2 3 4 5 6 7 8 9
| String jwt = Jwts.builder() .signWith(SignatureAlgorithm.HS256, "itheima") .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) .compact();
String jwt = JwtUtils.generateJwt(claims);
|
Redis缓存Token
流程
- 导入Spring Data Redis 的 maven 坐标
- 设置Redis数据源
- 编写配置类,创建RedisTemplate对象
- 通过RedisTemplate对象操作Redis
具体
1 2 3 4 5 6 7 8 9
|
@Autowired private StringRedisTemplate redisTemplate;
String jwt = JwtUtils.generateJwt(claims);
redisTemplate.opsForValue().set(jwt, 30, TimeUnit.MINUTES);
|
双令牌刷新策略

双令牌相比单令牌的优点
- 双令牌中 refresh_token 一天只需要一次
- 单令牌每次都要使用,所以暴露的概率更大
使用 Redis 分布式缓存存取评论数据
使用 RBAC 进行权限菜单树的开发,实现了管理员和权限逻辑分离
进行验证码的开发,使用 SHA256 对密码进行盐值加密
验证码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @WebServlet(name = "ServletCaptcha", value = "/captcha") public class ServletCaptcha extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { GifCaptcha captcha = CaptchaUtil.createGifCaptcha(150, 45, 4); String code = captcha.getCode(); request.getSession().setAttribute("code",code); captcha.write(response.getOutputStream()); } }
|
密码盐值加密(需修改)
引入 MD5Util.java 工具类
- MessageDigest.getInstance 生成加密计算摘要md
- 根据 md.digest() 计算得到8位字符串
- 通过 BigInteger() 返回十六进制字符
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
| package com.muyi.utils;
import java.math.BigInteger; import java.security.MessageDigest;
public class MD5Util { public static final String SALT = "yyds";
public static String getMD5String(String str) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(str.getBytes()); return new BigInteger(1, digest).toString(16); } catch (Exception e) { e.printStackTrace(); return null; } } }
|
AccountController.java类中
1 2 3 4 5 6 7 8 9 10 11 12
| @ApiOperation(value = "新增管理员") @PostMapping public Result add(@RequestBody Account Account){ log.info("新增管理员: {}" , Account); String s1 = MD5Util.getMD5String(Account.getPassword()); String pwdJM = MD5Util.getMD5String(s1 + MD5Util.SALT); Account.setPassword(pwdJM); AccountService.add(Account); return Result.success(); }
|
使用动态数据源对数据进行分库存储,使用 Interceptor 进行登录拦截
数据库设计
分表分库 的优点
- 减轻数据库的压力
- 业务分离
动态数据源引入 pom.xml
1 2 3 4 5
| <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.2</version> </dependency>
|
配置文件配置
可以在 springboot 中配置 master , base , order ,…
使用
在 mapper 层加注解 @DS
原理
AOP ,动态代理 ,
核心类
DynamicDataSourceAnnotationInterceptor implements MethodInterceptor
动态数据源注解拦截 实现 方法拦截
统一异常处理和拦截器
流程图
实现HandlerInterceptor
位置
interceptor目录下
具体流程
当我们打开浏览器来访问部署在web服务器当中的web应用时,此时我们所定义的过滤器会拦截到这次请求。拦截到这次请求之后,它会先执行放行前的逻辑,然后再执行放行操作。而由于我们当前是基于springboot开发的,所以放行之后是进入到了spring的环境当中,也就是要来访问我们所定义的controller当中的接口方法。
Tomcat并不识别所编写的Controller程序,但是它识别Servlet程序,所以在Spring的Web环境中提供了一个非常核心的Servlet:DispatcherServlet(前端控制器),所有请求都会先进行到DispatcherServlet,再将请求转给Controller。
当我们定义了拦截器后,会在执行Controller的方法之前,请求被拦截器拦截住。执行preHandle()
方法,这个方法执行完成后需要返回一个布尔类型的值,如果返回true,就表示放行本次操作,才会继续访问controller中的方法;如果返回false,则不会放行(controller中的方法也不会执行)。
在controller当中的方法执行完毕之后,再回过来执行postHandle()
这个方法以及afterCompletion()
方法,然后再返回给DispatcherServlet,最终再来执行过滤器当中放行后的这一部分逻辑的逻辑。执行完毕之后,最终给浏览器响应数据。
简略流程
- preHandle : 调用controller之前
- postHandle:调用controller之后
- afterCompletion:视图渲染之后(这里的视图渲染指的是服务器将动态资源转化为静态资源)
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Component public class LoginCheckInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle .... "); return true; }
@Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle ... "); }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion .... "); } }
|
配置拦截器
位置
config目录下
简略流程
- 实现 WebMvcConfigurer
- 重写 addInterceptors
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Configuration public class WebConfig implements WebMvcConfigurer {
@Autowired private LoginCheckInterceptor loginCheckInterceptor;
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login"); } }
|
使用 AliyunOSS 存取管理员头像,使用 PageHelper 插件实现分页查询
AliyunOSS
开通OSS服务
- 登录阿里云控制台,开通OSS服务
- 创建 Bucket(存储空间),选择合适的地域和存储类型 endpoint
获取访问密钥
- 在阿里云RAM访问控制中创建子账号
- 为子账号分配OSS读写权限
- 获取AccessKey ID和AccessKey Secret
后端配置
在 utils 工具包下导入AliOSSUtils类,并在该类下创建 upload 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public String upload(MultipartFile file) throws IOException {
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com"; String accessKeyId = "LTAI5tBhgTYGfP2be8vKf3Du"; String accessKeySecret = "oO1d4rMsMnANw2H3BWk24JqSf9Azr3"; String bucketName = "web-tlias-ycl-test";
|
创建uploadController
1 2 3 4 5 6 7 8 9 10
| @PostMapping("/upload") public Result upload(MultipartFile image) throws IOException { log.info("文件上传, 文件名: {}", image.getOriginalFilename());
String url = aliOSSUtils.upload(image); log.info("文件上传完成,文件访问的url: {}", url);
return Result.success(url);
|
使用 AOP 进行统一日志和声明式事务管理
AOP 进行统一日志
需求
统计各个业务层方法执行耗时。
实现步骤
- 导入依赖:在 pom.xml 中导入 AOP 的依赖
- 编写 AOP 程序:针对于特定方法根据业务需要进行编程
为演示方便,可以自建新项目或导入提供的springboot-aop-quickstart
项目工程
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.7</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
|
AOP程序:TimeAspect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Component @Aspect @Slf4j public class TimeAspect { @Around("execution(* com.itheima.service.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { long begin = System.currentTimeMillis();
Object result = pjp.proceed();
long end = System.currentTimeMillis();
log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);
return result; } }
|
声明式事务管理 @Transactional
作用
在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。
书写位置
- 方法:当前方法交给spring进行事务管理
- 类:当前类中所有的方法都交由spring进行事务管理
- 接口:接口下所有的实现类当中所有的方法都交给spring 进行事务管理
查看事务相关日志
在application.yml配置文件中开启事务管理日志
1 2 3 4
| logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug
|
重要参数
rollbackFor
默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper;
@Autowired private EmpMapper empMapper;
@Override @Transactional(rollbackFor=Exception.class) public void delete(Integer id){ deptMapper.deleteById(id); int num = id/0;
empMapper.deleteByDeptId(id); } }
|
propagation
propagation是用来配置事务的传播行为的。传播行为是指当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
属性值 |
含义 |
REQUIRED |
【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW |
【记录日志时使用】需要新事务,无论有无,总是创建新事务 |
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Service public class DeptLogServiceImpl implements DeptLogService {
@Autowired private DeptLogMapper deptLogMapper; @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void insert(DeptLog deptLog) { deptLogMapper.insert(deptLog); } }
|
业务实现类:DeptServiceImpl
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
| @Slf4j @Service
public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper;
@Autowired private DeptLogService deptLogService;
@Override @Log @Transactional(rollbackFor = Exception.class) public void delete(Integer id) throws Exception { try { deptMapper.deleteById(id); if(true){ throw new Exception("出现异常了~~~"); } empMapper.deleteByDeptId(id); }finally { DeptLog deptLog = new DeptLog(); deptLog.setCreateTime(LocalDateTime.now()); deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门"); deptLogService.insert(deptLog); } } }
|
设置 GlobalExceptionHandler 处理异常,并使用 Knife4j 文档进行后端接口测试
核心注解
- @ControllerAdvice :异常控制器,推荐使用@RestControllerAdvice(整体代码更简洁)
- @ExceptionHandler: 声明异常类型
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@RestControllerAdvice public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) public Result ex(Exception ex){ ex.printStackTrace(); return Result.error("对不起,操作失败,请联系管理员"); } }
|
引入配置
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>2.0.8</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
|
配置包下配置 Swagger 文档生成器
定义API文档的基本信息和扫描范围,使系统能够自动识别并生成指定包下控制器的API接口文档。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.itheima.config;
import org.springdoc.core.GroupedOpenApi; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Configuration public class SwaggerConfig {
@Bean public GroupedOpenApi publicApi() { return GroupedOpenApi.builder() .group("public-api") .pathsToMatch("/**") .build(); } }
|
配置包下配置 Knife4j 文档生成器
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
| package com.itheima.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@Configuration @EnableSwagger2WebMvc public class Knife4jConfig {
@Bean(value = "defaultApi2") public Docket defaultApi2() { Docket docket=new Docket(DocumentationType.SWAGGER_2) .apiInfo(new ApiInfoBuilder() .description("# 教学辅助系统 RESTful APIs") .termsOfServiceUrl("http://www.xx.com/") .contact("13017536058@163.com") .version("1.0") .build()) .groupName("2.X版本") .select() .apis(RequestHandlerSelectors.basePackage("com.itheima.controller")) .paths(PathSelectors.any()) .build(); return docket; }
}
|
修改配置类
1 2 3 4 5 6
| springdoc: api-docs: path: /v3/api-docs swagger-ui: path: /swagger-ui/index.html enabled: true
|
文档页面
- Swagger 原生界面:
http://localhost:8080/swagger-ui.html
- Knife4j 增强界面:
http://localhost:8080/doc.html
中文解释注解
@Api
: 用在类上,说明该类的作用
@ApiOperation
: 用在方法上,说明方法的作用
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
| package com.muyi.controller;
import com.muyi.pojo.entity.Result; import com.muyi.pojo.entity.Tag; import com.muyi.service.TagService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "标签模块") @Slf4j @CrossOrigin @RequestMapping("/tag") @RestController public class TagController { @Autowired private TagService tagService;
@ApiOperation(value = "查询全部标签") @GetMapping public Result list(){ log.info("查询全部标签数据"); List<Tag> tagList = tagService.list(); return Result.success(tagList); } }
|
使用 Vite 脚手架搭建 Vue 前端框架,并使用 ECharts 进行可视化分析
创建Vue3项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| # 使用 npm 安装 pnpm npm install -g pnpm pnpm -v # 应显示版本号,如 8.x.x
mkdir my-frontend-project cd my-frontend-project
pnpm init
pnpm create vite
# 安装必要依赖,可直接使用pnpm install # 核心库 pnpm add vue vue-router pinia # 开发工具 pnpm add -D vite @vitejs/plugin-vue # 常用工具库 pnpm add axios lodash-es # UI 库(以 Element Plus 为例) pnpm add element-plus @element-plus/icons-vue # CSS 预处理器 pnpm add -D sass
|
可视化分析(需修改)
安装echarts
全局引入 echarts.min.js
1 2 3 4 5 6 7
| import { createApp } from 'vue' import App from './App.vue' import * as echarts from 'echarts'
const app = createApp(App) app.config.globalProperties.$echarts = echarts app.mount('#app')
|
使用案例
核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13
| this.myChart.setOption({ series: [ { data: this.list.map(item => ({ value: item.price, name: item.name})) } ] })
|
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" /> <style> .red { color: red!important; } .search { width: 300px; margin: 20px 0; } .my-form { display: flex; margin: 20px 0; } .my-form input { flex: 1; margin-right: 20px; } .table > :not(:first-child) { border-top: none; } .contain { display: flex; padding: 10px; } .list-box { flex: 1; padding: 0 30px; } .list-box a { text-decoration: none; } .echarts-box { width: 600px; height: 400px; padding: 30px; margin: 0 auto; border: 1px solid #ccc; } tfoot { font-weight: bold; } @media screen and (max-width: 1000px) { .contain { flex-wrap: wrap; } .list-box { width: 100%; } .echarts-box { margin-top: 30px; } } </style> </head> <body> <div id="app"> <div class="contain"> <div class="list-box">
<form class="my-form"> <input v-model.trim="name" type="text" class="form-control" placeholder="消费名称" /> <input v-model.number="price" type="text" class="form-control" placeholder="消费价格" /> <button @click="add" type="button" class="btn btn-primary">添加账单</button> </form>
<table class="table table-hover"> <thead> <tr> <th>编号</th> <th>消费名称</th> <th>消费价格</th> <th>操作</th> </tr> </thead> <tbody> <tr v-for="(item, index) in list" :key="item.id"> <td>{{ index + 1 }}</td> <td>{{ item.name }}</td> <td :class="{ red: item.price > 500 }">{{ item.price.toFixed(2) }}</td> <td><a @click="del(item.id)" href="javascript:;">删除</a></td> </tr> </tbody> <tfoot> <tr> <td colspan="4">消费总计: {{ totalPrice.toFixed(2) }}</td> </tr> </tfoot> </table> </div> <div class="echarts-box" id="main"></div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script>
const app = new Vue({ el: '#app', data: { list: [], name: '', price: '' }, computed: { totalPrice () { return this.list.reduce((sum, item) => sum + item.price, 0) } }, created () {
this.getList() }, mounted () { this.myChart = echarts.init(document.querySelector('#main')) this.myChart.setOption({ title: { text: '消费账单列表', left: 'center' }, tooltip: { trigger: 'item' }, legend: { orient: 'vertical', left: 'left' }, series: [ { name: '消费账单', type: 'pie', radius: '50%', data: [ ], emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } } } ] }) },
methods: { async getList () { const res = await axios.get('https://applet-base-api-t.itheima.net/bill', { params: { creator: '小黑' } }) this.list = res.data.data
this.myChart.setOption({ series: [ { data: this.list.map(item => ({ value: item.price, name: item.name})) } ] }) }, async add () { if (!this.name) { alert('请输入消费名称') return } if (typeof this.price !== 'number') { alert('请输入正确的消费价格') return }
const res = await axios.post('https://applet-base-api-t.itheima.net/bill', { creator: '小黑', name: this.name, price: this.price }) this.getList()
this.name = '' this.price = '' }, async del (id) { const res = await axios.delete(`https://applet-base-api-t.itheima.net/bill/${id}`) this.getList() } } }) </script> </body> </html>
|