当前位置: 首页 > article >正文

【SpringBoot苍穹外卖】debugDay03.5

1、AOP面向切面编程

1. @Target(ElementType.METHOD)

  • 作用:指定自定义注解可以应用的目标范围。

  • 参数ElementType 是一个枚举类,定义了注解可以应用的目标类型。

    • ElementType.METHOD 表示该注解只能用于方法上。

    • 其他常见的 ElementType 值:

      • TYPE:类、接口、枚举等。

      • FIELD:字段(包括枚举常量)。

      • PARAMETER:方法参数。

      • CONSTRUCTOR:构造函数。

      • ANNOTATION_TYPE:注解类型。

      • PACKAGE:包。

      • 等等。

示例

@Target(ElementType.METHOD)
public @interface MyAnnotation {String value() default "";
}
  • 上面的 @MyAnnotation 注解只能用于方法上,如果尝试将其用于类或字段上,编译器会报错。


2. @Retention(RetentionPolicy.RUNTIME)

  • 作用:指定自定义注解的生命周期,即注解在什么阶段有效。

  • 参数RetentionPolicy 是一个枚举类,定义了注解的保留策略。

    • RetentionPolicy.RUNTIME:注解在运行时保留,可以通过反射机制读取。

    • RetentionPolicy.CLASS:注解在编译时保留,但运行时不可见(默认行为)。

    • RetentionPolicy.SOURCE:注解仅在源码阶段保留,编译后会被丢弃。

示例

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {String value() default "";
}
  • 上面的 @MyAnnotation 注解在运行时仍然有效,可以通过反射获取注解信息。


结合使用

通常,@Target 和 @Retention 会一起使用,以明确注解的作用范围和生命周期。

示例

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD) // 该注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时有效
public @interface MyAnnotation {String value() default "";
}

使用场景

public class MyClass {@MyAnnotation(value = "This is a method")public void myMethod() {System.out.println("Hello, World!");}
}

通过反射获取注解信息

import java.lang.reflect.Method;public class Main {public static void main(String[] args) throws Exception {Method method = MyClass.class.getMethod("myMethod");MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);if (annotation != null) {System.out.println("Annotation value: " + annotation.value());}}
}

输出

Annotation value: This is a method

总结

  • @Target(ElementType.METHOD):指定注解只能用于方法上。

  • @Retention(RetentionPolicy.RUNTIME):指定注解在运行时仍然有效,可以通过反射读取。

  • 这两个注解通常一起使用,用于定义自定义注解的行为和适用范围。

3、反射(Reflection)

 是 Java 提供的一种强大的机制,允许程序在运行时动态地获取类的信息(如类名、方法、字段、构造函数等),并操作这些信息(如调用方法、访问字段、创建对象等)。反射的核心思想是“在运行时检查和修改程序的行为”。

反射的主要用途包括:

  1. 动态加载类。

  2. 动态创建对象。

  3. 动态调用方法。

  4. 动态访问和修改字段。

  5. 获取注解信息。


反射的核心类

Java 反射的核心类和接口位于 java.lang.reflect 包中,主要包括:

  • Class<T>:表示一个类或接口,是反射的入口。

  • Method:表示类的方法。

  • Field:表示类的字段(成员变量)。

  • Constructor<T>:表示类的构造函数。

  • Annotation:表示注解。


反射的基本用法

1. 获取 Class 对象

Class 对象是反射的起点,可以通过以下方式获取:

  • Class.forName("全限定类名"):通过类的全限定名获取。

  • 对象.getClass():通过对象的实例获取。

  • 类名.class:通过类字面量获取。

示例

Class<?> clazz = Class.forName("com.example.MyClass");
2. 创建对象

通过 Class 对象可以动态创建类的实例。

示例

Class<?> clazz = Class.forName("com.example.MyClass");
Object obj = clazz.getDeclaredConstructor().newInstance();
3. 调用方法

通过 Method 对象可以动态调用类的方法。

示例

Class<?> clazz = Class.forName("com.example.MyClass");
Object obj = clazz.getDeclaredConstructor().newInstance();// 获取方法
Method method = clazz.getMethod("myMethod", String.class);// 调用方法
method.invoke(obj, "Hello, Reflection!");
4. 访问字段

通过 Field 对象可以动态访问和修改类的字段。

示例

Class<?> clazz = Class.forName("com.example.MyClass");
Object obj = clazz.getDeclaredConstructor().newInstance();// 获取字段
Field field = clazz.getDeclaredField("myField");// 设置字段可访问(如果是私有字段)
field.setAccessible(true);// 修改字段值
field.set(obj, "New Value");// 获取字段值
Object value = field.get(obj);
System.out.println(value);
5. 获取注解信息

通过反射可以获取类、方法、字段等元素上的注解信息。

示例

Class<?> clazz = Class.forName("com.example.MyClass");// 获取类上的注解
MyAnnotation classAnnotation = clazz.getAnnotation(MyAnnotation.class);
if (classAnnotation != null) {System.out.println("Class Annotation Value: " + classAnnotation.value());
}// 获取方法上的注解
Method method = clazz.getMethod("myMethod");
MyAnnotation methodAnnotation = method.getAnnotation(MyAnnotation.class);
if (methodAnnotation != null) {System.out.println("Method Annotation Value: " + methodAnnotation.value());
}

反射的优缺点

优点
  1. 灵活性:可以在运行时动态加载类、调用方法、访问字段,适合编写通用框架或工具。

  2. 扩展性:通过反射可以实现插件化架构,动态加载第三方类。

  3. 注解处理:反射是处理注解的基础,许多框架(如 Spring、MyBatis)都依赖反射来解析注解。

缺点
  1. 性能开销:反射操作比直接调用方法或访问字段要慢,因为涉及动态解析。

  2. 安全性问题:反射可以绕过访问控制(如访问私有字段或方法),可能导致安全问题。

  3. 代码可读性差:反射代码通常比较复杂,难以理解和维护。


反射的实际应用场景

  1. 框架开发:许多框架(如 Spring、Hibernate、MyBatis)都使用反射来实现依赖注入、动态代理、注解解析等功能。

  2. 单元测试:测试框架(如 JUnit)使用反射来调用测试方法。

  3. 动态加载类:在插件化架构中,通过反射动态加载第三方类。

  4. 序列化和反序列化:通过反射访问对象的字段,实现对象的序列化和反序列化。


示例代码

以下是一个完整的反射示例,展示了如何通过反射创建对象、调用方法和访问字段:

import java.lang.reflect.Field;
import java.lang.reflect.Method;public class ReflectionExample {public static void main(String[] args) throws Exception {// 获取 Class 对象Class<?> clazz = Class.forName("com.example.MyClass");// 创建对象Object obj = clazz.getDeclaredConstructor().newInstance();// 调用方法Method method = clazz.getMethod("myMethod", String.class);method.invoke(obj, "Hello, Reflection!");// 访问字段Field field = clazz.getDeclaredField("myField");field.setAccessible(true); // 设置私有字段可访问field.set(obj, "New Value");System.out.println("Field Value: " + field.get(obj));}
}class MyClass {private String myField = "Default Value";public void myMethod(String message) {System.out.println("Method Invoked: " + message);}
}

输出

Method Invoked: Hello, Reflection!
Field Value: New Value

总结

反射是 Java 中一种强大的机制,允许程序在运行时动态操作类和对象。虽然反射提供了极大的灵活性,但也带来了性能开销和安全性问题,因此应谨慎使用。在实际开发中,反射常用于框架开发、注解处理、动态加载类等场景。

4、idea快捷键ctrl + alt + b

快速转到接口实现方法。

5、实战

1 步骤一

自定义注解 AutoFill

进入到sky-server模块,创建com.sky.annotation包。

package com.sky.annotation;import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 自定义注解,用于标识某个方法需要进行功能字段自动填充处理*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {//数据库操作类型:UPDATE INSERTOperationType value();
}

其中OperationType已在sky-common模块中定义

package com.sky.enumeration;/*** 数据库操作类型*/
public enum OperationType {/*** 更新操作*/UPDATE,/*** 插入操作*/INSERT
}
2 步骤二

自定义切面 AutoFillAspect

在sky-server模块,创建com.sky.aspect包。

package com.sky.aspect;/*** 自定义切面,实现公共字段自动填充处理逻辑*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入点*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知,在通知中进行公共字段的赋值*/@Before("autoFillPointCut()")public void autoFill(JoinPoint joinPoint){/重要//可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解log.info("开始进行公共字段自动填充...");}
}

完善自定义切面 AutoFillAspect 的 autoFill 方法

package com.sky.aspect;import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;/*** 自定义切面,实现公共字段自动填充处理逻辑*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {/*** 切入点*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知,在通知中进行公共字段的赋值*/@Before("autoFillPointCut()")public void autoFill(JoinPoint joinPoint){log.info("开始进行公共字段自动填充...");//获取到当前被拦截的方法上的数据库操作类型MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象OperationType operationType = autoFill.value();//获得数据库操作类型//获取到当前被拦截的方法的参数--实体对象Object[] args = joinPoint.getArgs();if(args == null || args.length == 0){return;}Object entity = args[0];//准备赋值的数据LocalDateTime now = LocalDateTime.now();Long currentId = BaseContext.getCurrentId();//根据当前不同的操作类型,为对应的属性通过反射来赋值if(operationType == OperationType.INSERT){//为4个公共字段赋值try {Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);//通过反射为对象属性赋值setCreateTime.invoke(entity,now);setCreateUser.invoke(entity,currentId);setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {e.printStackTrace();}}else if(operationType == OperationType.UPDATE){//为2个公共字段赋值try {Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);//通过反射为对象属性赋值setUpdateTime.invoke(entity,now);setUpdateUser.invoke(entity,currentId);} catch (Exception e) {e.printStackTrace();}}}
}
3 步骤三

在Mapper接口的方法上加入 AutoFill 注解

CategoryMapper为例,分别在新增和修改方法添加@AutoFill()注解,也需要EmployeeMapper做相同操作

package com.sky.mapper;@Mapper
public interface CategoryMapper {/*** 插入数据* @param category*/@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +" VALUES" +" (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")@AutoFill(value = OperationType.INSERT)void insert(Category category);/*** 根据id修改分类* @param category*/@AutoFill(value = OperationType.UPDATE)void update(Category category);}

同时,将业务层为公共字段赋值的代码注释掉。

1). 将员工管理的新增和编辑方法中的公共字段赋值的代码注释。

2). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。

相关文章:

【SpringBoot苍穹外卖】debugDay03.5

1、AOP面向切面编程 1. Target(ElementType.METHOD) 作用&#xff1a;指定自定义注解可以应用的目标范围。 参数&#xff1a;ElementType 是一个枚举类&#xff0c;定义了注解可以应用的目标类型。 ElementType.METHOD 表示该注解只能用于方法上。 其他常见的 ElementType 值…...

分享在职同时准备系统分析师和教资考试的时间安排

&#xff08;在职、时间有限、同时备考系统分析师考试和小学信息技术教资面试&#xff09;&#xff0c;以下是详细的备考计划&#xff0c;确保计划的可行性和通过性。 一、总体安排 时间分配&#xff1a; 每周周末&#xff08;2天&#xff09;用于系统分析师考试备考。工作日晚…...

flink实时集成利器 - apache seatunnel - 核心架构详解

SeaTunnel&#xff08;原名 Waterdrop&#xff09;是一个分布式、高性能、易扩展的数据集成平台&#xff0c;专注于大数据领域的数据同步、数据迁移和数据转换。它支持多种数据源和数据目标&#xff0c;并可以与 Apache Flink、Spark 等计算引擎集成。以下是 SeaTunnel 的核心架…...

视频理解新篇章:Mamba模型的探索与应用

人工智能咨询培训老师叶梓 转载标明出处 想要掌握如何将大模型的力量发挥到极致吗&#xff1f;叶老师带您深入了解 Llama Factory —— 一款革命性的大模型微调工具&#xff08;限时免费&#xff09;。 1小时实战课程&#xff0c;您将学习到如何轻松上手并有效利用 Llama Facto…...

分形几何表明数学一直存在有首、末的无穷序列

分形几何表明数学一直存在有首、末的无穷序列。一有穷长直线段S可变为锯齿状图形G而由无穷多无穷短直线段连接而成。G和S一样有左、右两个端点。...

DeepSeek 的 API 服务引入 WPS Office

以下是将 DeepSeek 的 API 服务引入 WPS Office 的通用集成教程。以调用 DeepSeek 的 AI 功能&#xff08;如文本生成、数据分析&#xff09;为例&#xff0c;假设你需要通过 WPS 的宏或插件调用外部 API&#xff1a; 准备工作 注册 DeepSeek 账号并获取 API Key 访问 DeepSe…...

Python接口自动化测试—接口数据依赖

一般在做自动化测试时&#xff0c;经常会对一整套业务流程进行一组接口上的测试&#xff0c;这时候接口之间经常会有数据依赖&#xff0c;那又该如何继续呢&#xff1f; 那么有如下思路&#xff1a; 抽取之前接口的返回值存储到全局变量字典中。初始化接口请求时&#xff0c;…...

C++ 实践扩展(Qt Creator 联动 Visual Studio 2022)

​ 这里我们将在 VS 上实现 QT 编程&#xff0c;实现如下&#xff1a; 一、Vs 2022 配置&#xff08;若已安装&#xff0c;可直接跳过&#xff09; 点击链接&#xff1a;​​​​​Visual Studio 2022 我们先去 Vs 官网下载&#xff0c;如下&#xff1a; 等待程序安装完成之…...

分布式系统知识点总结

一、一致性协议 &#xffe5;1. CAP理论 CAP理论是分布式系统设计中的一套指导原则&#xff0c;它指出在网络分区的情况下&#xff0c;一个分布式系统最多只能同时满足以下三点中的两点&#xff1a; 一致性&#xff08;Consistency&#xff09;&#xff1a;所有节点在同一时…...

Java中性能瓶颈的定位与调优方法

Java中性能瓶颈的定位与调优方法 Java作为一种高效、跨平台的编程语言&#xff0c;广泛应用于企业级应用、服务器端开发、分布式系统等领域。然而&#xff0c;在面对大量并发、高负载的生产环境时&#xff0c;Java应用的性能瓶颈问题往往会暴露出来。如何定位并优化这些性能瓶…...

openbmc sdbusplus接口使用(持续更新...)

1.说明 本节介绍如何使用sdbusplus&#xff0c;用来对应不同的场景。 可以参考之前的文章: https://blog.csdn.net/wit_yuan/article/details/145192471 建议阅读本篇文章一定要仔细阅读sd-bus specification 2.说明 2.1 简单server服务注册 本节参考: https://gitee.com…...

2.12寒假作业

web&#xff1a;[HDCTF 2023]Welcome To HDCTF 2023 可以直接玩出来 但是这边还是看一下怎么解吧&#xff0c;看一下js代码&#xff0c;在js.game里面找到一个类似brainfuck加密的字符串 解密可以得到答案&#xff0c;但是后面我又去了解了一下let函数let命令、let命令 let命…...

GitHub项目推荐--适合练手的13个C++开源项目

1 C 那些事 这是一个适合初学者从入门到进阶的仓库&#xff0c;解决了面试者与学习者想要深入 C及如何入坑 C的问题。 除此之外&#xff0c;本仓库拓展了更加深入的源码分析&#xff0c;多线程并发等的知识&#xff0c;是一个比较全面的 C 学习从入门到进阶提升的仓库。 项目…...

【识别摄像头野外动物场景行为】

识别野外动物摄像头下的行为及动作&#xff0c;主要依赖于摄像头的拍摄质量、动物的行为特征以及可能的智能图像识别技术。以下是对这一过程的详细分析&#xff1a; 一、摄像头的作用与拍摄质量 监控与记录&#xff1a;野外动物摄像头&#xff0c;如红外相机&#xff0c;被广泛…...

Linux inode 详解

简介 索引节点&#xff08;Index Node&#xff09;是 Linux/类unix 系统文件系统上的一种数据结构&#xff0c;用于存储有关文件或目录的元数据。它包含文件的所有信息&#xff0c;除了文件名和数据。inode 在文件系统如何存储和检索数据方面起着至关重要的作用。 当在 Linux…...

程序员升级进阶之路

熟悉业务、项目代码、工作流程&#xff0c;积极吸取技术资料接需求&#xff0c;画流程图&#xff0c;&#xff08;伪代码&#xff09;&#xff0c;详细设计明确职业发展方向【很重要】求精&#xff1a;写代码前的技术方案设计 写代码并不难&#xff0c;关键是要明确为什么要写…...

linux下c++连接mysql

1、下载mysql客户端使用的库文件 sudo apt install libmysqlclient-dev 头文件一般在 /usr/include/mysql/ 下 库文件一般在 /usr/lib/x86_64-linux-gnu/ 下 2、mysql c api开发者指南 >>>>官方连接 3、API使用实例 #include<mysql/mysql.h> #include&…...

C语言基础入门:1.3编译流程与调试基础

编译流程与调试基础 ——从源代码到可执行文件的魔法解密 一、编译四重奏&#xff1a;代码的变身之旅 C程序的编译过程如同汽车组装流水线&#xff0c;分为四个精密阶段&#xff1a; 预处理&#xff08;Preprocessing&#xff09; gcc -E hello.c -o hello.i # 生成预处理文件…...

AcWing 792. 高精度减法

题目来源&#xff1a; AcWing - 算法基础课 题目内容&#xff1a; 给定两个正整数&#xff08;不含前导 0&#xff09;&#xff0c;计算它们的差&#xff0c;计算结果可能为负数。 输入格式 共两行&#xff0c;每行包含一个整数。 输出格式 共一行&#xff0c;包含所求的…...

Python爬虫实战:获取51job职位信息,并做数据分析

注意:以下内容仅供技术研究,请遵守目标网站的robots.txt规定,控制请求频率避免对目标服务器造成过大压力! 1. 环境准备 python import requests from bs4 import BeautifulSoup import pandas as pd import re import matplotlib.pyplot as plt 2. 爬虫核心代码(带反爬…...

【2025 Nature】AI 生成材料算法 MatterGen 文章要点

文章目录 1. MatterGen 框架2. 评价基础模型生成能力的指标3. MatterGen 基础生成能力表现4. MatterGen 定向生成能力表现i. 指定晶体化学式ii. 指定标量性质1. 每个性质微调一次。2. 两个性质联合微调 5. 实验合成6. 模型细节 这篇文档简单介绍 MatterGen 论文亮点。 标题&…...

时间序列分析(三)——白噪声检验

此前篇章&#xff1a; 时间序列分析&#xff08;一&#xff09;——基础概念篇 时间序列分析&#xff08;二&#xff09;——平稳性检验 一、相关知识点 白噪声的定义&#xff1a;白噪声序列是一种在统计学和信号处理中常见的随机过程&#xff0c;由一系列相互独立、具有相同…...

STM32-知识

一、Cortex-M系列双指针 Cortex-M系列的MSP与PSP有一些重要的区别&#xff0c;双指针是为了保证OS的安全性和稳健性。本质上&#xff0c;区别于用户程序使用PSP&#xff0c;操作系统和异常事件单独使用一个MSP指针的目的&#xff0c;是为了保证栈数据不会被用户程序意外访问或…...

将Mac上Python程序的虚拟环境搬到Windows

1. 导出Mac上Python虚拟环境的依赖 cd py && source venv/bin/activate && pip freeze > requirements.txt 2. 在Windows上创建一个新的虚拟环境 python -m venv venv 3. 激活虚拟环境 venv\Scripts\activate 4. 安装依赖 pip install -r requiremen…...

[前端] axios网络请求二次封装

一、场景描述 为什么要对axios网络请求进行二次封装? 解决代码的复用&#xff0c;提高可维护性。 —这个有两个方案&#xff1a;一个是二次封装一个是实例化。&#xff08;设置一些公共的参数&#xff0c;然后进行请求&#xff09; 为什么可以解决代码的复用&#xff1a; 这是…...

对前端的技术进行分层

前端相比较后端而言&#xff0c;由于其发展历史和浏览器的标准不一&#xff0c;导致其看上去简单&#xff0c;但是深入起来又很复杂&#xff0c;在最开始学习的时候&#xff0c;我们往往是了解一下三剑客和vue、react的api就开始上手工作了&#xff0c;但是到后面会发现&#x…...

【学术投稿-2025年计算机视觉研究进展与应用国际学术会议 (ACVRA 2025)】CSS样式解析:行内、内部与外部样式的区别与优先级分析

简介 2025年计算机视觉研究进展与应用&#xff08;ACVRA 2025&#xff09;将于2025年2月28-3月2日在中国广州召开&#xff0c;会议将汇聚世界各地的顶尖学者、研究人员和行业专家&#xff0c;聚焦计算机视觉领域的最新研究动态与应用成就。本次会议将探讨前沿技术&#xff0c;…...

【Linux】【网络】IO多路复用 select、poll、epoll

【Linux】【网络】IO多路复用 select、poll、epoll IO 多路复用 进程或线程同时监控多个文件描述符&#xff0c;查看描述符上是否有事件发生&#xff0c;从而提高资源利用率和系统吞吐量。 1. select int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exc…...

讲解下MySql的外连接查询在SpringBoot中的使用情况

在Spring Boot中使用MySQL的外连接查询时&#xff0c;通常通过JPA、MyBatis或JDBC等持久层框架来实现。外连接查询主要用于从多个表中获取数据&#xff0c;即使某些表中没有匹配的记录。外连接分为左外连接&#xff08;LEFT JOIN&#xff09;、右外连接&#xff08;RIGHT JOIN&…...

OpenGL-基础知识(更新中)

本文基于The Cherno在Youtube上的OpenGL系列视频总结出的笔记&#xff0c;等这个系列视频学习完后&#xff0c;将更加系统详细的学习《计算机图形学编程&#xff08;使用OpenGL和C 第二版&#xff09;》这本书。个人认为看cherno的视频上手速度更快&#xff0c;而且他对基本概念…...