基于自定义注解与AOP的精细化权限控制
🔐 基于自定义注解与AOP的精细化权限控制🌟 前言：权限控制的演进之路在分布式系统开发中，接口权限控制是保障系统安全的重要环节。传统基于数据库字段查询的权限控制方案存在几个明显缺陷：性能瓶颈：每次请求都需要查询数据库获取角色信息代码冗余：相同的校验逻辑分散在各个Controller中维护困难：权限规则变更需要修改多处业务代码以用户管理系统为例，我们需要区分管理员(admin)和普通用户(user)的权限。传统实现方式如下：// 传统实现方式的典型代码 @GetMapping("/admin/users") public List<User> getAllUsers(HttpServletRequest request) { // 每次都需要查询数据库校验权限 User loginUser = userService.getLoginUser(request); if (!"admin".equals(loginUser.getUserRole())) { throw new RuntimeException("无权限访问"); } return userService.list(); }本文将介绍如何通过自定义注解+Spring AOP实现声明式的权限控制，让权限校验逻辑与业务代码完美解耦。🧩 核心组件设计1. 自定义注解设计原理Java注解本质上是接口的扩展，通过@interface关键字定义。我们设计的@AuthCheck注解包含以下关键特性：@Target(ElementType.METHOD)：限定注解只能用于方法@Retention(RetentionPolicy.RUNTIME)：确保注解在运行时可通过反射获取mustRole属性：定义方法访问所需的最小权限@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthCheck { /** * 访问该方法必须拥有的角色 * @default "" 表示不需要特定角色 */ String mustRole() default ""; }2. AOP拦截器实现机制Spring AOP通过动态代理实现方法拦截。我们的AuthInterceptor核心功能包括：通过@Around注解定义切入点获取方法上的@AuthCheck注解配置从请求上下文获取当前用户执行角色权限校验决定是否放行或拦截@Aspect @Component public class AuthInterceptor { @Resource private UserService userService; @Around("@annotation(authCheck)") public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable { // 获取注解配置的必须角色 String mustRole = authCheck.mustRole(); // 获取当前请求对象 RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); // 获取当前登录用户 User loginUser = userService.getLoginUser(request); // 角色枚举转换 UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole); // 无需特定角色则直接放行 if (mustRoleEnum == null) { return joinPoint.proceed(); } // 获取用户角色 UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole()); // 角色校验失败处理 if (userRoleEnum == null) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 管理员权限特殊校验 if (UserRoleEnum.ADMIN.equals(mustRoleEnum) && !UserRoleEnum.ADMIN.equals(userRoleEnum)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } // 校验通过，执行原方法 return joinPoint.proceed(); } }🚀 实际应用示例管理员专属接口@RestController @RequestMapping("/admin") public class AdminController { @AuthCheck(mustRole = "admin") @GetMapping("/users") public List<User> getAllUsers() { return userService.list(); } @AuthCheck(mustRole = "admin") @PostMapping("/reset-password") public boolean resetPassword(@RequestParam Long userId) { return userService.resetPassword(userId); } }混合权限接口@RestController @RequestMapping("/api") public class UserController { // 无需特殊权限 @GetMapping("/profile") public User getProfile() { return userService.getCurrentUser(); } // 需要管理员权限 @AuthCheck(mustRole = "admin") @DeleteMapping("/user/{id}") public boolean deleteUser(@PathVariable Long id) { return userService.deleteById(id); } }📊 方案优势分析对比维度传统方式AOP+注解方式代码侵入性高低可维护性差（分散在各处）好（集中管理）开发效率低（重复编码）高（声明式配置）性能影响每次查询数据库可缓存权限信息可读性业务逻辑混杂关注点分离
Spring Boot万能脚手架：5分钟搭建企业级开发模板
为什么需要个人通用模板？提高项目初始化效率避免每次新建项目时重复配置基础依赖统一技术栈版本，减少兼容性问题快速集成常用组件（如MyBatis Plus等）统一项目规范预置代码风格配置统一的日志格式和收集方案标准化异常处理机制积累最佳实践封装经过验证的工具类沉淀业务通用解决方案记录踩坑经验与解决方案新建项目现在使用IDEA新建项目十分简单，直接左上角新建项目即可对于jdk版本，开发的要求当然是越稳定越好，所以我们可以选择11或者17但是注意，官方的Server Url不支持选择Java8和Java11，所以我们可以将上方的uel设置为https://start.aliyun.com/同样是因为稳定性需求，我们可以选择Springboot2.7.6版本，点击下一步，然后在此页面我们可以加入一些自己常用的依赖Spring Web、MySQL、Lombok自然是少不了,此时勾选即可选择完后直接创建即可添加依赖MyBatis-PlusMyBatis-Plus 是一个 MyBatis 的增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生在使用 MyBatis-Plus 时，通常不需要再单独引入 MyBatis 的依赖，因为 MyBatis-Plus 本身已经内置了 MyBatis 的核心依赖（即 mybatis 和 mybatis-spring），它会自动传递依赖进来具体可以看文档：https://baomidou.com/在Maven中的pom.xml中添加依赖<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.12</version> </dependency>添加完以来后我们需要配置一下，在resource文件夹下删除原来的配置文件，新建一个application.yml文件# Spring框架配置 # 配置Spring应用的基本信息和数据源连接 spring: # 应用配置 application: # 应用名称，用于服务注册与发现 name: 你的项目名字 # 数据源配置 datasource: # MySQL数据库驱动类名 driver-class-name: com.mysql.cj.jdbc.Driver # 数据库连接URL url: jdbc:mysql://localhost:3306/dmoye_picture # 数据库用户名 username: root # 数据库密码 password: xxxxx # MyBatis-Plus配置 # 配置MyBatis-Plus的映射规则和全局设置 mybatis-plus: # MyBatis配置项 configuration: #MyBatis配置 # 是否开启下划线到驼峰命名的自动转换 map-underscore-to-camel-case: false #仅在开发环境使用 # SQL执行日志实现类，用于控制台输出SQL语句 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # MyBatis-Plus全局配置 global-config: # 数据库配置 db-config: # 逻辑删除字段名 logic-delete-field: isDelete # 逻辑已删除值 logic-delete-value: 1 # 逻辑未删除值 logic-not-delete-value: 0我们配置好后，可以启动测试，发现项目可以正常启动Hutool工具库Hutool是一个 Java 工具类库，它封装了 Java 开发中常用的功能，提供了简单易用的 API，能够显著减少代码量，提高开发效率。Hutool 的目标是成为 Java 界的 "Swiss Army Knife"（瑞士军刀），涵盖工具方法、IO、加密、HTTP、缓存、JSON、Excel 等众多实用模块官方文档：https://doc.hutool.cn/pages/index在Maven中的pom.xml中添加依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.38</version> </dependency>Knife4j 接口文档Knife4j是一个为Java MVC框架集成Swagger生成API文档的增强解决方案，它集成了Swagger2和OpenAPI3规范，提供了一系列强大的文档展示和调试功能，让我们能够快速的调试我们开发的接口文档：https://doc.xiaominfo.com/docs/quick-start#spring-boot-2在Maven中的pom.xml中添加依赖<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> <version>4.4.0</version> </dependency>我们先创建一个controller包，然后配置yml属性，如下：# Knife4j接口文档配置 # 配置Knife4j Swagger增强工具的相关参数 knife4j: # 是否启用Knife4j enable: true # OpenAPI规范配置 openapi: # API文档标题 title: Knife4j官方文档 # API版本号 version: v1.0 # API分组配置 group: test1: # 分组名称 group-name: default # API规则类型 api-rule: package # API资源路径配置 api-rule-resources: # 控制器包路径 - com.domye.xxxx.controller #自己的controller路径 最后，访问Knife4j的文档地址：http://ip:port/doc.html即可查看文档通用基础代码自定义异常我们需要自定义错误码，对错误进行处理，便于前端管理我们新建一个exception包，用来存放异常处理相关的类@Getter public enum ErrorCode { SUCCESS(0, "ok"), PARAMS_ERROR(40000, "请求参数错误"), NOT_LOGIN_ERROR(40100, "未登录"), NO_AUTH_ERROR(40101, "无权限"), NOT_FOUND_ERROR(40400, "请求数据不存在"), FORBIDDEN_ERROR(40300, "禁止访问"), SYSTEM_ERROR(50000, "系统内部异常"), OPERATION_ERROR(50001, "操作失败"); /** * 状态码 */ private final int code; /** * 信息 */ private final String message; ErrorCode(int code, String message) { this.code = code; this.message = message; } }接下来，我们需要自定义一个异常类，就像空指针异常类、IO流异常类一样。此处自定义的异常类属于异常类，所有肯定是要继承一个异常类的，此处需要继承RuntimeException相比Exception来讲，RuntimeException是在程序运行时才会爆出异常，在编译时是不会出现异常的，这就表示，如果你throw了一个RuntimeException，不需要做额外操作；而throw一个Exception，程序会要求你try-catch，否则你根本启动不了程序，程序会提示（必须对其进行捕获或声明以便抛出）@Getter public class BusinessException extends RuntimeException { /* * 错误码 */ private final int code; public BusinessException(int code, String message) { super(message); this.code = code; } public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code = errorCode.getCode(); } public BusinessException(ErrorCode errorCode, String message) { super(message); this.code = errorCode.getCode(); } }对于这个代码，我们使用了super(message)，这是因为message 是 Throwable 的标准属性：Java 的异常体系中，Throwable 类（所有异常的基类）本身就有一个 detailMessage 字段用于存储异常信息。通过调用 super(message)，消息会自动存储在父类的这个标准字段中code 是业务扩展属性：错误码是业务层面的概念，Java 标准异常类中没有这个字段，所以需要在子类中自定义。而现在这样写，抛出异常时，需要throw new BusinessException(ErrorCode.PARAMS_ERROR);显然这样的写法不够优雅，也不够便捷,于是我们可以再进行封装一次public class ThrowUtils { /** * 条件成立则抛异常 * * @param condition 条件 * @param runtimeException 异常 */ public static void throwIf(boolean condition, RuntimeException runtimeException) { if (condition) { throw runtimeException; } } /** * 条件成立则抛异常 * * @param condition 条件 * @param errorCode 错误码 */ public static void throwIf(boolean condition, ErrorCode errorCode) { throwIf(condition, new BusinessException(errorCode)); } /** * 条件成立则抛异常 * * @param condition 条件 * @param errorCode 错误码 * @param message 错误信息 */ public static void throwIf(boolean condition, ErrorCode errorCode, String message) { throwIf(condition, new BusinessException(errorCode, message)); } }响应包装一般情况下，每个后端接口都要返回调用码、数据、调用信息等，前端可以根据这些信息进行相应的处理。我们可以封装统一的响应结果类，便于前端统一获取这些信息我们新建一个common包，创建BaseResponse类@Data public class BaseResponse<T> implements Serializable { private int code; private T data; private String message; public BaseResponse(int code, T data, String message) { this.code = code; this.data = data; this.message = message; } public BaseResponse(int code, T data) { this(code, data, ""); } public BaseResponse(ErrorCode errorCode) { this(errorCode.getCode(), null, errorCode.getMessage()); } }但之后每次接口返回值时，都要手动 new 一个 BaseResponse 对象并传入参数，比较麻烦，我们可以新建一个工具类，提供成功调用和失败调用的方法，支持灵活地传参，简化调用public class ResultUtils { /** * 成功 * @param data 数据 * @param <T> 数据类型 * @return 响应 */ public static <T> BaseResponse<T> success(T data) { return new BaseResponse<>(0, data, "ok"); } /** * 失败 * @param errorCode 错误码 * @return 响应 */ public static BaseResponse<?> error(ErrorCode errorCode) { return new BaseResponse<>(errorCode); } /** * 失败 * @param code 错误码 * @param message 错误信息 * @return 响应 */ public static BaseResponse<?> error(int code, String message) { return new BaseResponse<>(code, null, message); } /** * 失败 * @param errorCode 错误码 * @return 响应 */ public static BaseResponse<?> error(ErrorCode errorCode, String message) { return new BaseResponse<>(errorCode.getCode(), null, message); } }这样，我们返回的时候，就可以直接调用 ResultUtils.success(xxx)全局异常处理器为了防止意料之外的异常，利用 AOP 切面全局对业务异常和 RuntimeException 进行捕获：▼java复制代码@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public BaseResponse<?> businessExceptionHandler(BusinessException e) { log.error("BusinessException", e); return ResultUtils.error(e.getCode(), e.getMessage()); } @ExceptionHandler(RuntimeException.class) public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) { log.error("RuntimeException", e); return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误"); } }请求包装对于 分页这类通用的请求，可以封装统一的请求包装类，用于接受前端传来的参数，之后相同参数的请求就不用专门再新建一个类了分页请求包装类，接受页号、页面大小、排序字段、排序顺序参数：@Data public class PageRequest { /** * 当前页号 */ private int current = 1; /** * 页面大小 */ private int pageSize = 10; /** * 排序字段 */ private String sortField; /** * 排序顺序（默认降序） */ private String sortOrder = "descend"; }全局跨域配置跨域是指浏览器访问的 URL（前端地址）和后端接口地址的域名（或端口号）不一致导致的，浏览器为了安全，默认禁止跨域请求访问。为了开发调试方便，我们可以通过全局跨域配置，让整个项目所有的接口支持跨域，解决跨域报错。新建 config 包，用于存放所有的配置相关代码。全局跨域配置代码如下：@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 覆盖所有请求 registry.addMapping("/**") // 允许发送 Cookie .allowCredentials(true) // 放行哪些域名（必须用 patterns，否则 * 会和 allowCredentials 冲突） .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .exposedHeaders("*"); } }编写示例接口移除 controller 包下的其他代码，让项目干净一些，然后编写一个纯净的 /health 接口用于健康检查：@RestController @RequestMapping("/") public class MainController { /** * 健康检查 */ @GetMapping("/health") public BaseResponse<String> health() { return ResultUtils.success("ok"); } }健康检查是指可以通过访问该接口，来快速验证后端服务是否正常运行，所以该接口的返回值非常简单。
Vue Router 的 history 模式 使用的是 纯前端路由，即 URL 路径（如 /about）并不对应服务器上的真实文件或目录，而是由 JavaScript 动态渲染内容而传统 后端路由（如 PHP、Java）的 /about 可能对应一个真实的 about.php 或 about.html 文件当用户直接访问或刷新页面时：浏览器会向服务器请求 /about但服务器上并没有 about 这个文件或接口Nginx 默认返回 404（因为找不到匹配的资源）因此需要配置nginxlocation / { try_files $uri $uri/ /index.html; }它的执行逻辑是：$uri → 先检查请求的路径是否对应真实文件（如 /about → /about.html 或 /about/index.html）如果存在，直接返回该文件如果不存在，进入下一步$uri/ → 检查请求的路径是否对应一个目录（如 /about/）如果存在，尝试返回该目录下的默认文件（如 index.html）如果不存在，进入下一步/index.html → 如果前两步都失败，最终返回 index.htmlVue 会解析 URL 路径（如 /about），并渲染对应的前端页面这样，无论是直接访问 /about 还是刷新页面，Nginx 都会返回 index.html，由 Vue Router 处理路由逻辑。
前段时间我用vue写了一个个人主页，但是主页的配置总是要修改，每次都要自己构建好再上传，我就思考有没有一种方法能够自动化部署通过GitHub Actions自动化部署或许可行，我研究了一下，参考了CSDN上的文章，加上Deepseek搜索，找到了完美的解决方案配置SSH密钥密钥可以直接通过宝塔面板获取生成公钥和私钥当然，这种方法并不安全，有权限管理相关问题我们可以生成一个，方便管理ssh-keygen -t rsa -b 4096 -C "github-actions@yourdomain.com" -f ~/.ssh/github_actions_rsa -N ""参数解释：-t rsa: 指定密钥类型为RSA-b 4096: 指定密钥长度为4096位(更安全)-C: 添加注释，通常用邮箱作为标识-f: 指定密钥文件名称和路径-N "": 设置空密码，便于自动化流程这个命令会在~/.ssh/目录下生成两个文件：github_actions_rsa (私钥，必须保密)github_actions_rsa.pub (公钥，可公开)配置公钥cat ~/.ssh/github_actions_rsa.pub ##查看公钥 mkdir -p ~/.ssh echo "公钥内容" >> ~/.ssh/authorized_keys chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keysGitHub仓库配置设置Repository Secrets在GitHub仓库中配置以下机密变量：进入仓库 → Settings → Secrets and variables → Actions添加以下secrets：名称值说明SERVER_IP服务器IP地址如：123.456.789.0USERNAMESSH用户名一般为rootPRIVATE_KEYSSH私钥内容完整的私钥文件内容SERVER_DESTINATION部署目录路径如：/www/wwwroot/testGitHub Actions工作流配置在项目根目录创建.github/workflows/deploy.yml文件：name: Vue Deploy # 工作流名称 on: push: branches: - main # 仅当推送至 main 分支时触发部署 jobs: build-and-deploy: runs-on: ubuntu-latest # 使用 GitHub 提供的最新 Ubuntu 环境 steps: # 1. 检出代码仓库 - name: Checkout repository uses: actions/checkout@v4 # 使用官方 Action 检出代码 # 2. 安装 Node.js 环境 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "20.11" # 指定 Node.js 版本,可以自选 # 3. 安装项目依赖 - name: Install dependencies run: npm install # 安装项目所需的 npm 包 # 4. 构建并压缩 dist 内容 - name: Build and Zip run: | sudo apt-get update && sudo apt-get install -y zip # 安装 zip 工具 npm run build # 执行 Vue 项目构建 cd dist # 进入 dist 目录 zip -r ../dist-content.zip ./* # 压缩 dist 内容（不包含 dist 文件夹） # 5. 上传 dist-content.zip（可选，用于调试） - name: Upload dist-content.zip uses: actions/upload-artifact@v4 with: name: dist-content # 构建产物名称 path: dist-content.zip # 构建产物路径 # 6. 通过 SCP 将 dist-content.zip 传输到服务器 - name: Transfer dist-content.zip via SCP uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.SERVER_IP }} # 服务器 IP（从 GitHub Secrets 获取） username: ${{ secrets.USERNAME }} # SSH 用户名（从 GitHub Secrets 获取） key: ${{ secrets.PRIVATE_KEY }} # SSH 私钥（从 GitHub Secrets 获取） source: "dist-content.zip" # 本地文件路径 target: ${{ secrets.SERVER_DESTINATION}} # 服务器目标目录（从 GitHub Secrets 获取） # 7. SSH 登录服务器并执行部署脚本 - name: Deploy via SSH uses: appleboy/ssh-action@v0.1.7 with: host: ${{ secrets.SERVER_IP}} username: ${{ secrets.USERNAME }} key: ${{ secrets.PRIVATE_KEY }} script: | shopt -s extglob # 启用 Bash 的扩展 glob 模式（支持 !(pattern) 语法） DEPLOY_DIR=${{ secrets.SERVER_DESTINATION }} # 从 GitHub Secrets 获取部署目录路径 cd $DEPLOY_DIR # 进入部署目录 rm -rf !(dist-content.zip) # 删除所有文件，但保留 dist-content.zip unzip -o dist-content.zip # 解压 dist-content.zip（-o 保留符号链接） rm -f dist-content.zip # 删除 zip 文件（可选） nginx -s reload # 重载 Nginx 服务（确保 Nginx 配置正确）
路由简单使用安装固定5个固定的步骤（不用死背，熟能生巧）下载 VueRouter 模块到当前工程yarn add vue-router@3.6.5在main.js中import VueRouter from 'vue-router'//引入VueRouter Vue.use(VueRouter)//安装注册 const router = new VueRouter()//创建路由对象 new Vue({ render: h => h(App), router:router//将路由对象注入到new Vue实例中 }).$mount('#app')当我们配置完以上5步之后 就可以看到浏览器地址栏中的路由 变成了 /#/的形式。表示项目的路由已经被Vue-Router管理了配置创建好需要的组件 (views目录)，配置路由规则const router = new VueRouter({ // routes 路由规则们 // route 一条路由规则 { path: 路径, component: 组件 } routes: [ { path: '/find', component: Find }, { path: '/my', component: My }, { path: '/friend', component: Friend }, ] }) export default router每条路由规则是一个对象，包含：path：URL 路径（如 /find）component：路径匹配时渲染的组件（需提前导入组件如 Find）而export default router将配置好的路由实例导出，供 Vue 主文件（如 main.js）注入到根实例中import Vue from 'vue' import App from './App.vue' import router from './router/index' new Vue({ render: h => h(App), router }).$mount('#app')声明式导航router-link标签vue-router 提供了一个全局组件 router-link (取代 a 标签)能跳转，配置 to 属性指定路径(必须) 。本质还是 a 标签 ，to 无需 #能高亮，默认就会提供高亮类名，可以直接设置高亮样式语法： <router-link to="path的值">发现音乐</router-link> <router-link to="/find">发现音乐</router-link> <router-link to="/my">我的音乐</router-link> <router-link to="/friend">朋友</router-link> <router-view></router-view> router-link类名使用router-link跳转后，我们发现。当前点击的链接默认加了两个class的值 router-link-exact-active和router-link-active在 Vue Router 中，router-link-active 和 router-link-exact-active 是两个非常重要的 CSS 类名，它们由 Vue Router 自动添加到 <router-link> 组件上，用于指示当前导航链接的激活状态：router-link-active触发条件：当路由路径部分匹配时添加匹配规则：只要当前路由的路径包含该 <router-link> 的目标路径，就会添加示例： html <router-link to="/user">用户</router-link> 当访问 /user → 激活 - 当访问 /user/profile → 激活（因为包含 /user）当访问 /product → 不激活router-link-exact-active触发条件：当路由路径完全匹配时添加匹配规则：只有当当前路由的路径完全等于该 <router-link> 的目标路径时才会添加示例： html <router-link to="/user">用户</router-link> 当访问 /user → 激活 - 当访问 /user/profile → 不激活（路径不完全相等）当访问 /users → 不激活（即使相似但不相等）自定义类名router-link的两个高亮类名太长了，我们可以在创建路由对象时，额外配置两个配置项const router = new VueRouter({ linkActiveClass: 'active', // 配置模糊匹配的类名 linkExactActiveClass: 'exact-active' // 配置精确匹配的类名 })查询参数传参通过 URL 的 ? 后拼接参数（如 /user?id=123）参数可见，适合非敏感数据（如分页、筛选条件）不需要提前在路由配置中定义参数名传参 <router-link to="/user?id=123&name=Alice"></router-link> <router-link :to="{ path: '/user', query: { id: 123, name: 'Alice' } }"> </router-link>接收export default { mounted() { // 通过 $route.query 获取 console.log(this.$route.query.id) // 输出 123 console.log(this.$route.query.name) // 输出 'Alice' } }动态路由传参这种方式需要配置路由，必须使用 : 定义参数名const router = new VueRouter({ routes: [ ..., { path: '/search/:words', //定义参数名 component: Search , name: xxx//定义别名，可选 } ] })传参 <router-link to="/user/123">用户123</router-link> <router-link :to="{ name: 'user', params: { id: 123 } }"> </router-link>其中，第二种写法需路由配置 name 属性接收参数export default { props: ['id'], // 需路由配置 props: true mounted() { // 方式1：通过 props（推荐） console.log(this.id) // 输出 123 // 方式2：通过 $route.params console.log(this.$route.params.id) // 输出 123 } }两种传参方式对比对比项查询参数（Query）动态路由参数（Params）URL 形式/path?key=value/path/:id（如 /user/123）路由配置无需特殊配置需定义 :param 占位符参数可见性明文显示在 URL部分隐藏（仅路径中显示值）传参方式query: { key: value }params: { key: value }组件获取方式this.$route.querythis.$route.params 或 props适用场景筛选、分页等可选参数必选参数（如用户ID、文章ID）用查询参数（Query）当：参数是可选的（如 ?page=1）需要保留参数在 URL 中可见（如分享链接）用动态路由（Params）当：参数是必选的（如用户详情页 /user/123）需要更简洁的 URL（符合 RESTful 风格）重定向网页打开时， url 默认是 / 路径，未匹配到组件时，会出现空白const router = new VueRouter({ routes: [ { path: '/', redirect: '/home'}, ... ] })404404的路由，虽然配置在任何一个位置都可以，但一般配置在其他路由规则的后面即前面不匹配就命中最后const router = new VueRouter({ routes: [ ... { path: '*', component: NotFound } ] })编程式导航跳转编程式导航即用JS代码来进行跳转//简单写法 this.$router.push('路由路径') //完整写法 this.$router.push({ path: '路由路径' })也可以通过name来进行跳转，配置name配置项{ name: '路由名', path: '/path/xxx', component: XXX },this.$router.push({ name: '路由名' })查询参数传参传参// 方式1：path + query this.$router.push({ path: '/search', query: { keyword: 'vue', page: 1 } // 参数对象 }) // 方式2：命名路由 + query this.$router.push({ name: 'search', query: { keyword: 'vue' } }) // 方式3：URL 字符串（不推荐） this.$router.push('/search?keyword=vue&page=1')接收export default { mounted() { // 通过 $route.query 获取 console.log(this.$route.query.keyword) // 'vue' console.log(this.$route.query.page) // '1' } }动态路由传参传参// 方式1：命名路由 + params this.$router.push({ name: 'user', // 必须用 name！ params: { id: 123 } // 参数对象 }) // 方式2：路径字符串（不推荐，维护性差） this.$router.push('/user/123')接收export default { props: ['id'], // 需路由配置 props: true mounted() { // 方式1：通过 props（推荐） console.log(this.id) // 方式2：通过 $route.params console.log(this.$route.params.id) } }Vuex核心使用安装安装vuex与vue-router类似，vuex是一个独立存在的插件，如果脚手架初始化没有选 vuex，就需要额外安装npm i vuex@3新建 store/index.js 专门存放 vuex为了维护项目目录的整洁，在src目录下新建一个store目录其下放置一个index.js文件。 (和 router/index.js 类似)​ 创建仓库 store/index.js// 导入 vue import Vue from 'vue' // 导入 vuex import Vuex from 'vuex' // vuex也是vue的插件, 需要use一下, 进行插件的安装初始化 Vue.use(Vuex) // 创建仓库 store const store = new Vuex.Store() // 导出仓库 export default store在 main.js 中导入挂载到 Vue 实例上import Vue from 'vue' import App from './App.vue' import store from './store' Vue.config.productionTip = false new Vue({ render: h => h(App), store }).$mount('#app')成功创建了一个 空仓库StateState提供唯一的公共数据源，所有共享的数据都要统一放到Store中的State中存储。打开项目中的store.js文件，在state对象中可以添加我们要共享的数据。// 创建仓库 store const store = new Vuex.Store({ state: { count: 101 } })state 状态, 即数据, 类似于vue组件中的datastate 中的数据整个vue项目的组件都能访问到组件中可以使用 $store 获取到vuex中的store对象实例，可通过state属性属性获取count state的数据 - {{ $store.state.count }} 组件逻辑中使用，把state中数据，定义在组件内的计算属性中 state的数据 - {{ count }} computed: { count () { return this.$store.state.count } }js文件中使用，需要先引入storeimport store from "@/store" console.log(store.state.count)MutationsMutations 的核心职责是直接修改 Vuex store 的状态（state）mutations: { addCount (state, count) { state.count = count } }其中state 参数是必须的在组件中发送请求更新state，需要使用$store.commit，其中传参可以是对象this.$store.commit('addCount', 10) this.$store.commit('addCount', { count: 10 })若要传递多个数据，只能通过传对象的方式actionsstate是存放数据的，mutations是同步更新数据 (便于监测数据的变化, 更新视图等, 方便于调试工具查看变化)，actions则负责进行异步操作定义actionsmutations: { changeCount (state, newCount) { state.count = newCount } } actions: { setAsyncCount (context, num) { // 一秒后, 给一个数, 去修改 num setTimeout(() => { context.commit('changeCount', num) }, 1000) } },组件中通过dispatch调用setAsyncCount () { this.$store.dispatch('setAsyncCount', 666) }getters除了state之外，有时我们还需要从state中筛选出符合条件的一些数据，这些数据是依赖state的，此时会用到getters例如，state中定义了list，为1-10的数组，state: { list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }组件中，需要显示所有大于5的数据，正常的方式，是需要list在组件中进行再一步的处理，但是getters可以帮助我们实现它 getters: { // getters函数的第一个参数是 state // 必须要有返回值 filterList: state => state.list.filter(item => item > 5) }调用语法也很简单 {{ $store.getters.filterList }} Vuex辅助函数mapStatemapState 是 Vuex（Vue.js 的状态管理库）中的一个辅助函数，用于将 Vuex store 中的 state 属性快速映射到组件的计算属性中。它可以简化组件中获取全局状态的代码，避免重复声明计算属性import { mapState } from 'vuex' export default { computed: { ...mapState(['count', 'message']) } }将 store.state.count 映射为 this.count将 store.state.message 映射为 this.message可直接{{count}}调用mapMutationsmapMutations和mapState很像，它把位于mutations中的方法提取了出来，我们可以将它导入import { mapMutations } from 'vuex' methods: { ...mapMutations(['addCount']) }此时，就可以直接通过this.addCount调用了 值+1 mapActionsmapActions 是把位于 actions中的方法提取了出来，映射到组件methods中import { mapActions } from 'vuex' methods: { ...mapActions(['changeCountAction']) } 直接通过 this.方法 就可以调用 +异步 Vuex模块化由于使用单一状态树，应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时，store 对象就有可能变得相当臃肿。这句话的意思是，如果把所有的状态都放在state中，当项目变得越来越大的时候，Vuex会变得越来越难以维护由此，又有了Vuex的模块化准备 state定义模块 user 和 settinguser中管理用户的信息状态 userInfo modules/user.jsconst state = {} export default { state, }在store/index.js文件中的modules配置项中，注册模块import user from './modules/user' import setting from './modules/setting' const store = new Vuex.Store({ modules:{ user, } })使用模块中的数据, 可以直接通过模块名访问 $store.state.模块名.xxx => $store.state.setting.desc也可以通过 mapState 映射获取模块内的state数据尽管已经分模块了，但其实子模块的状态，还是会挂到根级别的 state 中，属性名就是模块名访问方式有两种直接通过模块名访问 $store.state.模块名.xxx通过 mapState 映射：mapState('模块名', ['xxx']) - 要开启命名空间 namespaced:trueconst state = {} export default { namespaced: true, state, }$store直接访问$store.state.user.userInfo.namemapState辅助函数访问...mapState('user', ['userInfo']), ...mapState('setting', ['theme', 'desc']),获取模块内的getters数据使用模块中 getters 中的数据：直接通过模块名访问 $store.getters['模块名/xxx ']通过 mapGetters 映射：mapGetters('模块名', ['xxx']) - 需要开启命名空间const getters = { // 分模块后，state指代子模块的state UpperCaseName (state) { return state.userInfo.name.toUpperCase() } }直接访问 {{ $store.getters['user/UpperCaseName'] }} 通过命名空间访问computed:{ ...mapGetters('user', ['UpperCaseName']) }获取模块内的mutations方法直接通过 store 调用 $store.commit('模块名/xxx ', 额外参数)通过 mapMutations 映射 ：mapMutations('模块名', ['xxx']) - 需要开启命名空间const mutations = { setUser (state, newUserInfo) { state.userInfo = newUserInfo } }直接访问methods: { updateUser () { $store.commit('模块名/mutation名', 额外传参) }, }通过命名空间访问methods:{ ...mapMutations('setting', ['setTheme']), }获取模块内的actions方法直接通过 store 调用 $store.dispatch('模块名/xxx ', 额外参数)通过 mapActions 映射:子模块的映射 mapActions('模块名', ['xxx']) - 需要开启命名空间const actions = { setUserSecond (context, newUserInfo) { setTimeout(() => { context.commit('setUser', newUserInfo) }, 1000) } }直接通过store调用methods:{ update () { this.$store.dispatch('user/setUserSecond', { name: 'xiaohong', age: 28 }) }, }mapActions映射methods:{ ...mapActions('user', ['setUserSecond']) }
工程化开发和脚手架开发Vue的两种方式核心包传统开发模式：基于html / css / js 文件，直接引入核心包，开发 Vue。工程化开发模式：基于构建工具（例如：webpack）的环境中开发Vue。工程化开发模式优点：提高编码效率，比如使用JS新语法、Less/Sass、Typescript等通过webpack都可以编译成浏览器识别的ES3/ES5/CSS等工程化开发模式问题：webpack配置不简单雷同的基础配置缺乏统一的标准为了解决以上问题，所以我们需要一个工具，生成标准化的配置脚手架Vue CLIVue CLI 是Vue官方提供的一个全局命令工具可以帮助我们快速创建一个开发Vue项目的标准化基础架子全局安装（只需安装一次即可） yarn global add @vue/cli 或者 npm i @vue/cli -g查看vue/cli版本： vue --version创建项目架子：vue create project-name(项目名不能使用中文)启动项目：yarn serve 或者 npm run serve(命令不固定，找package.json)项目目录介绍和运行流程项目目录介绍虽然脚手架中的文件有很多，目前咱们只需认识三个文件即可main.js 入口文件App.vue App根组件index.html 模板文件运行流程根组件根组件介绍整个应用最上层的组件，包裹所有普通小组件组件是由三部分构成template：结构 （有且只能一个根元素）script: js逻辑style： 样式 (可支持less，需要装包)组件注册局部注册局部注册的组件只能在注册的组件内使用创建.vue文件（三个组成部分）在使用的组件内先导入再注册，最后使用当成html标签使用即可 <组件名></组件名>组件名规范 —> 大驼峰命名法， 如 HmHeader// 导入需要注册的组件 import 组件对象 from '.vue文件路径' import HmHeader from './components/HmHeader' export default { // 局部注册 components: { '组件名': 组件对象, HmHeader:HmHeaer, HmHeader } }全局注册全局注册的组件，在项目的任何组件中都能使用创建.vue组件（三个组成部分）main.js中进行全局注册// 导入需要全局注册的组件 import HmButton from './components/HmButton' Vue.component('HmButton', HmButton)局部样式写在组件中的样式会 全局生效 ，因此很容易造成多个组件之间的样式冲突问题。可以给组件的style标签加上scoped 属性,可以让样式只作用于当前组件即 添加后，当前组件内标签都被添加data-v-hash值 的属性 ，css选择器都被添加 [data-v-hash值] 的属性选择器因此必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到 组件通信组件通信，就是指组件与组件之间的数据传递组件的数据是独立的，无法直接访问其他组件的数据。想使用其他组件的数据，就需要组件通信父子通信父组件通过 props 将数据传递给子组件子组件利用 $emit 通知父组件修改更新父向子通信父组件 我是APP组件 <Son :title="myTitle"></Son> 子组件 我是Son组件 {{title}} 传值步骤给子组件以添加属性的方式传值子组件内部通过props接收模板中直接使用 props接收的值子向父通信子组件利用 $emit 通知父组件，进行修改更新父组件 我是APP组件 <Son :title="myTitle" @changTitle="handleChange"></Son> 传值步骤$emit触发事件，给父组件发送消息通知父组件监听$emit触发的事件提供处理函数，在函数的性参中获取传过来的参数Propsprops 是组件间数据传递的核心机制，用于父组件向子组件传递数据单向数据流 Props 数据只能从父组件流向子组件，子组件不能直接修改 props（会触发警告）声明式接收 子组件需显式声明它接受的 props动态/静态传递 父组件可传递静态值或动态绑定的数据Props 的声明方式数组形式（简单声明）export default { props: ['title', 'content', 'isPublished'] }对象形式export default { props: { title: String, // 基础类型检查 // 高级配置 content: { type: String, // 数据类型 required: true, // 是否必传 default: 'Default content', // 默认值 validator: value => value.length > 10 // 自定义验证函数 } } }props校验完整写法props: { 校验的属性名: { type: 类型, // Number String Boolean ... required: true, // 是否必填 default: 默认值, // 默认值 validator (value) { // 自定义校验逻辑 return 是否通过校验 } } },2.代码实例export default { props: { w: { type: Number, //required: true, default: 0, validator(val) { // console.log(val) if (val >= 100 || val <= 0) { console.error('传入的范围必须是0-100之间') return false } else { return true } }, }, }, }.sync修饰符可以实现 子组件 与 父组件数据 的 双向绑定，简化代码<BaseDialog :visible.sync="isShow" ></BaseDialog>子组件props: { visible: Boolean }, this.$emit('update:visible', false)代码示例 退出按钮 <!-- isShow.sync => :isShow="isShow" @update:isShow="isShow=$event" --> <BaseDialog :isShow.sync="isShow"></BaseDialog> 温馨提示： x 你确认要退出本系统么？ 确认 取消 插槽让组件内部的一些 结构 支持 自定义基本组件内需要定制的结构部分，改用占位使用组件时, 标签内部, 传入结构替换slot给插槽传入内容时，可以传入纯文本、html标签、组件通过插槽完成了内容的定制，传什么显示什么, 但是如果不传，则是空白能否给插槽设置 默认显示内容 呢？封装组件时，可以为预留的 <slot> 插槽提供后备内容（默认内容）。在 标签内，放置内容, 作为默认显示内容 <MyDialog></MyDialog> <MyDialog> 你确认要退出么 </MyDialog> 子组件 <slot> 我是默认的文本内容 </slot> 具名一个组件内有多处结构，需要外部传入标签，进行定制 上面的弹框中有三处不同，但是默认插槽只能定制一个位置多个slot使用name属性区分名字 <slot name="head"></slot> <slot name="content"></slot> <slot name="footer"></slot> template配合v-slot:名字来分发对应标签 <MyDialog> 我是大标题 我是内容 取消 确认 </MyDialog> v-slot写起来太长，vue给我们提供一个简单写法 v-slot: —> #作用域定义slot 插槽的同时, 是可以传值的。给 插槽 上可以 绑定数据，将来 使用组件时可以用给 slot 标签, 以 添加属性的方式传值<slot :id="item.id" msg="测试文本"></slot>所有添加的属性, 都会被收集到一个对象中{ id: 3, msg: '测试文本' }在template中, 通过 #插槽名= "obj" 接收，默认插槽名为 default<MyTable :list="list"> 删除 </MyTable>
内容渲染指令内容渲染指令用来辅助开发者渲染 DOM 元素的文本内容。常用的内容渲染指令有如下2 个：v-text（类似innerText）使用语法： hello ，意思是将 uame 值渲染到 p 标签中类似 innerText，使用该语法，会覆盖 p 标签原有内容v-html（类似 innerHTML）使用语法： hello ，意思是将 intro 值渲染到 p 标签中类似 innerHTML，使用该语法，会覆盖 p 标签原有内容类似 innerHTML，使用该语法，能够将HTML标签的样式呈现出来 个人信息 姓名： 简介： 条件渲染指令条件判断指令，用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个，分别是：v-show作用： 控制元素显示隐藏语法： v-show = "表达式" 表达式值为 true 显示， false 隐藏原理： 切换 display:none 控制显示隐藏场景：频繁切换显示隐藏的场景v-if作用： 控制元素显示隐藏（条件渲染）语法： v-if= "表达式" 表达式值 true显示， false 隐藏原理： 基于条件判断，是否创建 或 移除元素节点场景： 要么显示，要么隐藏，不频繁切换的场景v-else 和 v-else-if作用：辅助v-if进行判断渲染语法：v-else v-else-if="表达式"需要紧接着v-if使用事件绑定指令使用Vue时，如需为DOM注册事件，及其的简单，语法如下： 按钮 按钮 按钮 v-on:可简写为 @ - + + 属性绑定指令作用：动态设置html的标签属性 比如：src、url、title语法：v-bind:属性名=“表达式”v-bind:可以简写成 => :比如，有一个图片，它的 src 属性值，是一个图片地址。这个地址在数据 data 中存储。则可以这样设置属性值： 控制样式为了方便开发者进行样式控制， Vue 扩展了 v-bind 的语法，可以针对 class 类名 和 style 行内样式 进行控制 。 :class = "对象/数组">这是一个div 当class动态绑定的是对象时，键就是类名，值就是布尔值，如果值是true，就有这个类，否则没有这个类, 适用一个类名，来回切换 当class动态绑定的是数组时 → 数组中所有的类，都会添加到盒子上，本质就是一个 class 列表，可用于批量添加或删除类 对有样式控制的增强 {{ percent }}% 设置25% 设置50% 设置75% 设置100% 列表渲染指令Vue 提供了 v-for 列表渲染指令，用来辅助开发者基于一个数组来循环渲染一个列表结构。v-for 指令需要使用 (item, index) in arr 形式的特殊语法，其中：item 是数组中的每一项index 是每一项的索引，不需要可以省略arr 是被遍历的数组 {{ item.name }} {{ item.author }} 删除 给列表项添加的唯一标识。便于Vue进行列表项的正确排序复用。Vue 的默认行为会尝试原地修改元素（就地复用）,加key可避免key 的值只能是字符串 或 数字类型，且值必须具有唯一性双向绑定指令数据改变后，呈现的页面结果会更新页面结果更新后，数据也会随之而变作用： 给表单元素（input、radio、select）使用，双向绑定数据，可以快速 获取 或 设置 表单元素内容语法：v-model="变量" 账户： 密码： 登录 重置 指令修饰按键修饰符@keyup.enter —>当点击enter键的时候才触发v-model修饰符v-model.trim —>去除首位空格v-model.number —>转数字事件修饰符@事件名.stop —> 阻止冒泡@事件名.prevent —>阻止默认行为@事件名.stop.prevent —>可以连用 即阻止事件冒泡也阻止默认行为ref和$refs在 Vue.js 2 中，ref 和 $refs 是用于直接访问 DOM 元素或子组件实例的特殊属性。它们提供了一种脱离响应式系统的直接操作方式，适用于需要手动控制 DOM 或调用子组件方法的场景。ref 属性作用：在模板中标记一个 DOM 元素或子组件，Vue 会将其引用注册到 $refs 对象中。语法： Hello <child-component ref="myChild"></child-component>$refs作用：通过 this.$refs 访问所有被 ref 标记的元素或组件实例。特性：非响应式（修改 ref 不会触发视图更新）。仅在组件渲染完成后填充（避免在 created 生命周期中访问）。访问 DOM 元素 聚焦输入框 调用子组件方法 <child-component ref="childRef" /> 触发子组件方法 computed计算属性在 Vue.js 2 中，计算属性（Computed Properties） 是一种声明式的属性，用于基于组件数据（data）动态计算并返回新值。它们具有缓存机制，能高效处理依赖数据变化时的复杂逻辑核心特点声明式依赖追踪 自动追踪响应式依赖（如 data 中的属性），依赖变化时自动重新计算。缓存机制 依赖未变化时，直接返回缓存结果，避免重复计算（性能优势）。响应式更新 计算结果也是响应式的，可触发视图更新。代码简洁 将复杂逻辑抽取为属性，模板更清晰。基本用法在 Vue 组件中定义 computed 选项：new Vue({ data() { return { firstName: '张', lastName: '三' } }, computed: { // 计算属性：全名 fullName() { return this.firstName + ' ' + this.lastName; } } })模板中像普通属性一样使用： 全名：{{ fullName }} 高级用法计算属性默认的简写，只能读取访问，不能 "修改"如果要 "修改" → 需要写计算属性的完整写法计算属性默认只有 getter，但可提供 setter 实现双向绑定：computed: { fullName: { get() { return this.firstName + ' ' + this.lastName; }, set(newValue) { // 当执行 this.fullName = "李 四" 时触发 const names = newValue.split(' '); this.firstName = names; this.lastName = names || ''; } } }watch侦听器侦听器（Watch） 用于监听响应式数据的变化，并在变化时执行特定的逻辑（如异步操作、复杂计算或副作用处理）。与计算属性（computed）不同，watch 更适用于响应数据变化执行操作的场景，而非派生新数据核心特点监听数据变化 可监听 data、computed 或 props 中的属性，变化时触发回调。执行异步操作 适合处理需要等待 API 返回、定时任务等异步逻辑。精细化控制 支持深度监听（deep）、立即触发（immediate）等配置。副作用处理 适用于数据变化时需要修改其他数据或触发外部操作的情况。基本用法在 Vue 组件的 watch 选项中定义侦听器：Vue({ data() { return { count: 0, user: { name: '张三', age: 25 } }; }, watch: { // 监听基本数据类型 count(newVal, oldVal) { console.log(`count变化：从${oldVal}变为${newVal}`); }, // 监听对象属性（需用字符串路径） 'user.name'(newName) { console.log('用户名变为：', newName); } } });高级配置通过对象语法配置侦听器的行为：watch: { user: { handler(newVal, oldVal) { console.log('user对象变化', newVal); }, deep: true, // 深度监听对象内部变化 immediate: true // 立即触发一次回调（初始值） } }为什么需要深度监听？当监听一个对象或数组时，如果直接修改其内部属性（如 obj.key = newValue 或 arr = 1），由于对象/数组的引用地址未变，浅层监听不会触发回调。此时需要通过 deep: true 启用深度监听假设有一个嵌套对象 user：data() { return { user: { name: '张三', address: { city: '北京' } } }; }默认浅层监听（不生效）JavaScriptwatch: { user(newVal, oldVal) { console.log('user变化', newVal); // 仅当 this.user = {...} 整体替换时触发 } }❌ 以下操作不会触发侦听器：this.user.name = '李四'; // 修改属性 this.user.address.city = '上海'; // 修改嵌套属性启用深度监听（生效）watch: { user: { handler(newVal, oldVal) { console.log('user变化', newVal); // 任何嵌套属性修改都会触发 }, deep: true // 关键配置 } }✅ 以下操作会触发侦听器：this.user.name = '李四'; // 触发 this.user.address.city = '上海'; // 触发 this.user = { name: '王五' }; // 触发（整体替换）
顺序表 用一组连续的内存单元依次存储线性表的各个元素，也就是说，逻辑上相邻 的元素，实际的物理存储空间也是连续的。存储结构#define MAXSIZE 20 //线性表存储空间的初始分配量 #define OK 1 //成功标识 #define ERROR 0 //失败标识 typedef int Status; //Status是函数的类型，其值是函数结果状态代码，如OK等 typedef int ElemType; //ElemType的类型根据实际情况而定，这里假定为int typedef struct { ElemType *elem; //存储 int length; }SqList;即其中maxsize为线性表存储空间的初始值初始化Status InitList(SqList* L){ //构造一个空的线性表L L -> elem = (ElemType *)malloc(MAXSIZE*sizeof(ElemType)); if(!L -> elem){ // 如果分配失败 return ERROR; } L -> length = 0; return OK; }插入Status ListInsert(SqList *L, int i, ElemType e){ int k; if (L->length == MAXSIZE){ //线性表已满 return ERROR; } if (i < 1 || i > L->length+1){ //当i不在范围内时 return ERROR; } if (i <= L->length){ //若插入位置不在表尾 for(k = L->length-1;k >= i-1;k--){ L->elem[k+1] = L->elem[k]; } } L->elem[i-1] = e; //将新元素插入 L->length++; //长度加1 return OK; }插入，即将原来的元素都向后移动一位删除元素同理，删除元素即所有元素向左移动一位Status ListDelete(SqList *L, int i, ElemType *e){ if(L->length == 0){ //线性表为空 return ERROR; } if(i < 1 || i > L->length){ //删除位置不正确 return ERROR; } *e = L -> elem[i-1]; if(i < L->length){ //如果删除位置不在最后位置 for(int k; = i;k < L->length;k++){ L->elem[k-1] = L->elem[k]; } } L->length--; //长度减1 return OK; }*e的作用：保存被删除的元素值查找Status GetElem(SqList L, int i, ElemType *e){ if(L.length == 0 || i<1 || i>L.length){ return ERROR; } *e = L.elem[i-1]; return OK; }读取所有元素void OutPut(SqList L){ printf("当前顺序表的长度:%d\n", L.length); for(int i = 0; i < L.length; i++){ printf("%d ",L.elem[i]); } printf("\n"); }小结线性表的顺序存储结果在读、存数据是的时间复杂度是O(1)，插入、删除操作的时间复杂度是O(n)无须为表中元素之间的逻辑关系而增加额外的存储空间；可以快速的存取表中任一位置的元素但是当插入和删除操作需要移动大量元素时，若线性表长度较大，难以确定存储空间的容量；造成存储空间的“碎片”单链表概念在链式结构中，除了要存储数据元素的信息外，还要存储它的后继元素的存储地址为了表示每个数据元素ai与其直接后继元素ai+1之间的逻辑关系，对数据ai来说除了存储其本身的信息之外，还需要存储一个指示其直接后继的信息（即直接后继的存储位置）我们把存储数据元素信息的域称为数据域，把存储直接后继位置的域称为指针域指针域中存储的信息称做指针或链这两部分信息组成数据元素ai的存储映像，称为结点（Node）n个结点（ai的存储映像）链结成一个链表，即为线性表（a1, a2, …, an）的链式存储结构因为此链表的每个结点中只包含一个指针域，所以叫做单链表存储结构typedef int ElemType; typedef struct node{ ElemType data; struct node *next; }Node初始化Node* initList() { Node *head = (Node*)malloc(sizeof(Node)); head->data = 0; head->next = NULL; return head; } int main() { Node *list = initList(); return 1; }头插法int insertHead(Node* L, ElemType e) { Node *p = (Node*)malloc(sizeof(Node)); p->data = e; p->next = L->next; L->next = p; }先开辟出一个内存空间存储节点pp的数据域存入数据将p的指针域指向原来头部的指针域再将头部的指针域指向p在指定位置插入int insertNode(Node* L, int pos, ElemType e) { Node* p = L; // p指向头节点（假设L是带头节点的链表） int i = 0; // 移动p到目标位置的前一个节点（pos-1） while (i < pos-1) { // 循环条件调整为i < pos，确保pos=0时也能正确处理 p = p->next; i++; if (p == NULL) { // 若p提前为空，说明pos超出链表长度 return 0; } } Node* q = (Node*)malloc(sizeof(Node)); q->data = e; q->next = p->next; // 新节点指向p的下一个节点 p->next = q; // p指向新节点，完成插入 return 1; // 插入成功 }删除节点找到要删除节点的前置节点 p用指针 q 记录要删除的节点通过改变 p 的后继节点实现删除释放删除节点的空间int deleteNode(Node *L, int pos) { Node *p = L; int i = 0; while(i < pos-1) { p = p->next; i++; if (p == NULL) { return 0; } } if(p->next == NULL) { printf("要删除的位置错误\n"); return 0; } Node *q = p->next; p->next = q->next; q->next->prev = p; free(q); return 1; }获取长度int listLength(Node *L) { Node *p = L; int len = 0; while(p != NULL) { p = p->next; len++; } return len; }释放链表指针 p 指向头节点后的第一个节点判断指针 p 是否指向空节点如果 p 不为空，用指针 q 记录指针 p 的后继节点释放指针 p 指向的节点指针 p 和指针 q 指向同一个节点，循环上面的操作void freeList(Node *L) { Node *p = L->next; Node *q; while(p != NULL) { q = p->next; free(p); p = q; } L->next = NULL; }循环列表循环链表（Circular Linked List）是另一种形式的链式存储结构其特点是表 中最后一个节点的指针域指向头节点，整个链表形成一个环当链表遍历时，判别当前指针 p 是否指向表尾结点的终止条件不同在单链表 中，判别条件为 p!=NULL 或 p->next!=NULL而循环单链表的判别条件为 p!=L 或 p->next!=L。
分布式锁分布式锁是用于协调分布式系统中多个进程服务互斥访问共享资源的机制。其核心目标是确保在分布式环境下，同一时刻只有一个客户端能操作共享资源（如数据库、文件等），以解决并发导致的数据不一致问题 。核心特性互斥性：同一时刻仅有一个客户端持有锁，其他请求需等待或失败 。超时机制：锁自动过期释放，避免因客户端故障导致死锁（如 Redis 的 PX 参数） 。可重入性：同一线程可重复获取同一把锁（如 Redisson 通过 Hash 结构记录线程标识和重入次数） 。高可用与容错：锁服务需支持集群部署，避免单点故障（如 Redis 的 RedLock 算法、ZooKeeper 集群） 。常见实现方式基于数据库唯一约束：通过插入唯一键（如业务单号）实现加锁，删除记录释放锁 。悲观锁：使用 SELECT ... FOR UPDATE 锁定数据行 。缺点：性能低、死锁风险高，仅适用于低并发场景 。基于 RedisSETNX + EXPIRE：SET key value NX PX 3000 原子性加锁，Lua 脚本保证解锁原子性 。Redisson 框架：支持可重入锁、看门狗自动续期（默认每 10 秒重置超时）、订阅机制实现阻塞等待 。缺点：主从切换时可能丢锁（RedLock 算法通过多节点投票解决） 。基于 ZooKeeper临时顺序节点：客户端创建临时顺序节点，仅最小序号节点获锁；其他节点监听前序节点释放事件 。Curator 框架：封装 InterProcessMutex 实现可重入锁，节点断开自动删除临时节点 。缺点：性能低于 Redis，但强一致性和顺序性更可靠 。简易锁以Spring Data Redis为例，用RedisTemplate来操作Redis// 加锁 public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) { return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit); } // 解锁，防止删错别人的锁，以uuid为value校验是否自己的锁 public void unlock(String lockName, String uuid) { if(uuid.equals(redisTemplate.opsForValue().get(lockName)) { redisTemplate.opsForValue().del(lockName); } }这是一个简易的锁，但是加锁和删锁不是原子性的，并发一旦大了，无法保证线程安全线程A执行get操作，获取到锁的值"uuidA"，判断与自己持有的uuid相等，准备执行del。此时，锁的过期时间到了（或者因为其他原因，比如Redis的过期策略），锁自动失效（即被Redis删除）。线程B此时尝试获取锁"lock1"，由于锁已失效，线程B成功获取锁（setIfAbsent成功），设置值为"uuidB"。线程A继续执行del操作，删除了锁"lock1"（此时删除的是线程B刚设置的锁）。结果：线程B的锁被线程A删除，导致线程B在持有锁的过程中，锁被意外删除，其他线程（比如线程C）可能又会获得锁，导致多个线程同时进入临界区。Lua脚本保证原子性Lua脚本是redis已经内置的一种轻量小巧语言，其执行是通过redis的eval/evalsha命令来运行，把操作封装成一个Lua脚本，如论如何都是一次执行的原子操作if redis.call('get', KEYS[1]) == ARGV[1] then -- 执行删除操作 return redis.call('del', KEYS[1]) else -- 不成功，返回0 return 0 end// 解锁脚本 DefaultRedisScript<Object> unlockScript = new DefaultRedisScript(); unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockDel.lua"))); // 执行lua脚本解锁 redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value);而这种写法，同一个线程不允许获取同一把锁，在有些场景不适用，例如public void methodA() { lock.lock(); // 第一次加锁 try { // 业务逻辑1... methodB(); // 调用methodB } finally { lock.unlock(); // 最终释放锁（需确保只释放一次） } } public void methodB() { lock.lock(); // 嵌套调用，需要再次加锁 try { // 业务逻辑2... } finally { lock.unlock(); } }如果锁是不可重入的，当methodA获取锁后调用methodB时，methodB尝试再次加锁会被阻塞（因为锁已被当前线程持有），导致死锁，由此引出可重入锁可重入锁Redisson的可重入锁通过自动维护重入次数解决了这一问题：当同一线程首次获取锁时，重入次数初始化为1；若同一线程再次获取同一把锁（如嵌套调用），重入次数递增（如2、3...）；释放锁时，重入次数递减，直到次数为0时才真正释放锁（通知Redis删除锁）。开发者无需手动维护重入次数，锁的获取和释放逻辑与普通单次加锁一致，大大降低了复杂度。由此，我们可以改造lua脚本redis存储锁名称，该锁线程id和对应线程进入次数每次线程获取锁时，判断是否获取过锁，若获取过，次数+1解锁时，判断是否已存在该锁，有则-1，然后判断次数是否为0，若为0则删除该锁存储结构为了方便维护这个对象，我们用Hash结构来存储这些字段。Redis的Hash类似Java的HashMap，适合存储对象lockname 锁名称 key1： threadId 唯一键，线程id value1： count 计数器，记录该线程获取锁的次数计数器当同一个线程获取同一把锁时，我们需要对对应线程的计数器count做加减判断一个redis key是否存在，可以用exists，而判断一个hash的key是否存在，可以用hexists而redis也有hash自增的命令hincrby，每次自增1时 hincrby lockname1 threadId 1，自减1时 hincrby lockname1 threadId -1解锁加锁部分代码local key = KEYS[1]; local threadId = ARGV[1]; local releaseTime = ARGV[2]; -- lockname不存在 if(redis.call('exists', key) == 0) then redis.call('hset', key, threadId, '1'); redis.call('expire', key, releaseTime); return 1; end; -- 当前线程已id存在 if(redis.call('hexists', key, threadId) == 1) then redis.call('hincrby', key, threadId, '1'); redis.call('expire', key, releaseTime); return 1; end; return 0;解锁部分代码local key = KEYS[1]; local threadId = ARGV[1]; -- lockname、threadId不存在 if (redis.call('hexists', key, threadId) == 0) then return nil; end; -- 计数器-1 local count = redis.call('hincrby', key, threadId, -1); -- 删除lock if (count == 0) then redis.call('del', key); return nil; end;这种写法在面临一些场景时依旧会出现问题A进程在获取到锁的时候，因业务操作时间太长，锁释放了但是业务还在执行，而此刻B进程又可以正常拿到锁做业务操作，两个进程操作就会存在依旧有共享资源的问题如果负责储存这个分布式锁的Redis节点宕机以后，而且这个锁正好处于锁住的状态时，这个锁会出现锁死的状态所以我们希望在这种情况时，可以延长锁的releaseTime延迟释放锁来直到完成业务期望结果，这种不断延长锁过期时间来保证业务执行完成的操作就是锁续约，由此引出Redisson分布式锁Redisson介绍Redisson是一个高级的分布式协调Redis客服端，能帮助用户在分布式环境中轻松实现一些Java的对象。Redisson、Jedis、Lettuce 是三个常用的操作 Redis 的客户端，Jedis、Lettuce 的 API 更侧重对 Reids 数据库的 CRUD（增删改查）；而 Redisson API 侧重于分布式开发，提供了多种分布式Java对象和服务，包括分布式锁、分布式集合、分布式对象、分布式限流器、可重入锁等。安装依赖<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.6</version> </dependency>配置@Configuration public class RedissionConfig { @Value("${spring.redis.host}") private String redisHost; @Value("${spring.redis.password}") private String password; private int port = 6379; @Bean public RedissonClient getRedisson() { Config config = new Config(); config.useSingleServer(). setAddress("redis://" + redisHost + ":" + port). setPassword(password); config.setCodec(new JsonJacksonCodec()); return Redisson.create(config); } }启用@Resource private RedissonClient redissonClient; RLock rLock = redissonClient.getLock(lockName); try { boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS); if (isLocked) { // TODO } } catch (Exception e) { rLock.unlock(); }redissonClient是Redisson的核心客户端实例，用于连接Redis服务器并操作分布式对象getLock(lockName)通过客户端获取一个分布式锁对象（RLock），lockName是锁的唯一标识符RLock接口继承自java.util.concurrent.locks.Lock，提供分布式环境下的可重入锁能力RLock从RLock进入，找到RedissonLock类，找到tryLock方法再递进到干事的tryAcquireOnceAsync方法private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { if (leaseTime != -1L) { return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG); } else { RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining == null) { this.scheduleExpirationRenewal(threadId); } } }); return ttlRemainingFuture; } }分析有leaseTime时间判断的2个分支加锁先分析有过期时间tryLockInnerAsync部分<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { this.internalLockLeaseTime = unit.toMillis(leaseTime); return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS [1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[] { this.internalLockLeaseTime, this.getLockName(threadId) }); }观察可得，这里返回的是用eval命令执行Lua脚本，此处的脚本展开得-- 不存在该key时 if (redis.call('exists', KEYS[1]) == 0) then -- 新增该锁并且hash中该线程id对应的count置1 redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 设置过期时间 redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; -- 存在该key 并且 hash中线程id的key也存在 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 线程重入次数++ redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);​ 分析参数可得// 锁名称 KEYS[1] = Collections.singletonList(this.getName()) // 锁失效时间 ARGV[1] = this.internalLockLeaseTime // uuid+threadId组合的唯一值 ARGV[2] = this.getLockName(threadId)判断该锁是否已经有对应hash表存在，没有对应的hash表：则set该hash表中一个entry的key为锁名称，value为1，之后设置该hash表失效时间为leaseTime存在对应的hash表：则将该lockName的value执行+1操作，也就是计算进入次数，再设置失效时间leaseTime最后返回这把锁的ttl剩余时间解锁protected RFuture<Boolean> unlockInnerAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); return 1; end;if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getName(), this.getChannelName()), new Object[] { LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId) }); }-- 不存在key if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; -- 计数器 -1 local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then -- 过期时间重设 redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else -- 删除并发布解锁消息 redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;如果该锁不存在则返回nil；如果该锁存在则将其线程的hash key计数器-1，计数器counter>0，重置下失效时间，返回0；否则，删除该锁，发布解锁消息unlockMessage，返回1；分析可得，该lua有两个key，即Arrays.asList(this.getName(), this.getChannelName())name 锁名称channelName，用于pubSub发布消息的channel名称ARGV变量有三个LockPubSub.UNLOCK_MESSAGE,internalLockLeaseTime和 getLockName(threadId)LockPubSub.UNLOCK_MESSAGE，channel发送消息的类别，此处解锁为0internalLockLeaseTime，watchDog配置的超时时间，默认为30slockName,这里的lockName指的是uuid和threadId组合的唯一值续约其中unLock的时候使用到了Redis发布订阅PubSub完成消息通知。而订阅的步骤就在RedissonLock的加锁入口的lock方法里long threadId = Thread.currentThread().getId(); Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);//尝试获取分布式锁，返回锁的剩余存活时间 if (ttl != null) { // 订阅 RFuture<RedissonLockEntry> future = this.subscribe(threadId);//向 Redis 订阅当前锁的释放事件 if (interruptibly) {//是否可中断 this.commandExecutor.syncSubscriptionInterrupted(future); } else { this.commandExecutor.syncSubscription(future); }当锁被其他线程占用时，通过监听锁的释放通知（在其他线程通过RedissonLock释放锁时，会通过发布订阅pub/sub功能发起通知），等待锁被其他线程释放进入LockPubSub，这里只有一个明显的监听方法onMessage，其订阅和信号量的释放都在父类PublishSubscribe，我们只关注监听事件的实际操作protected void onMessage(RedissonLockEntry value, Long message) { Runnable runnableToExecute; if (message.equals(unlockMessage)) { // 从监听器队列取监听线程执行监听回调 runnableToExecute = (Runnable) value.getListeners().poll(); if (runnableToExecute != null) { runnableToExecute.run(); } // getLatch()返回的是Semaphore，信号量，此处是释放信号量 // 释放信号量后会唤醒等待的entry.getLatch().tryAcquire去再次尝试申请锁 value.getLatch().release(); } else if (message.equals(readUnlockMessage)) { while (true) { runnableToExecute = (Runnable) value.getListeners().poll(); if (runnableToExecute == null) { value.getLatch().release(value.getLatch().getQueueLength()); break; } runnableToExecute.run(); } } }LockPubSub监听最终执行了2件事runnableToExecute.run() 执行监听回调value.getLatch().release(); 释放信号量Redisson通过LockPubSub监听解锁消息，执行监听回调和释放信号量通知等待线程可以重新抢锁。这时再回来看tryAcquireOnceAsync另一分支else { RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN); ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } });可以看到，无过期时间时，在执行加锁操作后，还执行了一段逻辑ttlRemainingFuture.onComplete((ttlRemaining, e) -> { if (e == null) { if (ttlRemaining) { this.scheduleExpirationRenewal(threadId); } } })执行异步加锁的操作后，加锁成功则根据加锁完成返回的ttl是否过期来确认是否执行一段定时任务查看RedissonLock.this.scheduleExpirationRenewal(threadId)private void scheduleExpirationRenewal(long threadId) { // 1. 创建锁续期任务入口 ExpirationEntry entry = new ExpirationEntry(); // 2. 原子操作：注册续期任务到全局Map（KEY=锁名称） ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry); if (oldEntry != null) { // 3. 重入场景：同一线程多次获取锁时，仅记录线程ID oldEntry.addThreadId(threadId); } else { // 4. 首次加锁：记录线程ID并启动续期任务 entry.addThreadId(threadId); renewExpiration(); // 核心续期逻辑 } } private void renewExpiration() { ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ee == null) return; // 锁已释放 // 1. 创建定时任务（默认间隔 = 锁超时时间/3，即10秒） Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { @Override public void run(Timeout timeout) { ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ent == null) return; // 2. 获取锁持有者线程ID（首次加锁线程） Long threadId = ent.getFirstThreadId(); if (threadId == null) return; // 3. 异步续期：通过Lua脚本延长锁超时时间 RFuture<Boolean> future = renewExpirationAsync(threadId); future.onComplete((res, e) -> { if (e != null) { log.error("续期失败", e); // 网络异常等 } else if (res) { renewExpiration(); // 递归调用实现循环续期 } // else: 锁已被释放或线程失去所有权 }); } }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 默认30s/3=10s ee.setTimeout(task); // 绑定任务到ExpirationEntry }renewExpirationAsync 的Lua如下protected RFuture<Boolean> renewExpirationAsync(long threadId) { return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)}); } if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;因为锁的失效时间是30s，当10s之后，此时这个timeTask 就触发了，他就去进行续约，把当前这把锁续约成30s，如果操作成功，那么此时就会递归调用自己，再重新设置一个timeTask()，于是再过10s后又再设置一个timerTask，完成不停的续约流程即A、B线程争抢一把锁，A获取到后，B阻塞B线程阻塞时并非主动CAS，而是PubSub方式订阅该锁的广播消息A操作完成释放了锁，B线程收到订阅消息通知B被唤醒开始继续抢锁，拿到锁
在.vscode文件中的launch.json文件中修改相关配置加入"vmArgs": "-Dserver.port=8081 -Denv=replica"如：{ "type": "java", "name": "Service-8081 (Replica)", "request": "launch", "cwd": "${workspaceFolder}", "mainClass": "com.hmdp.HmDianPingApplication", "projectName": "hm-dianping", "envFile": "${workspaceFolder}/.env", "vmArgs": "-Dserver.port=8081 -Denv=replica", "args": "" }其中 name决定名字，vmArgs中port决定端口然后打开左侧边栏的调试，选择相应的启动即可