深入理解JDK动态代理原理,使用javassist动手写一个动态代理框架
文章目录
- 一、动手实现一个动态代理框架
- 1、初识javassist
- 2、使用javassist实现一个动态代理框架
- 二、JDK动态代理
- 1、编码实现
- 2、基本原理
- (1)getProxyClass0方法
- (2)总结
- 写在后面
一、动手实现一个动态代理框架
1、初识javassist
Java操纵字节码,最底层一般是使用ASM进行操作的,但是ASM上手难度很大,我们可以使用javassist对字节码进行操作,相对来说简单一些。
下面这个实例,就是我们使用javassist对接口动态生成一个实现类:
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;public class JavassistDemo {public static void main(String[] args) throws Exception {TestService proxy = createProxy();proxy.sayHello("zhangsan"); // hello:zhangsan}/*** 生成一个TestService的实现类*/public static TestService createProxy() throws Exception {// javassist 底层是ASM,ASM底层是编辑JVM指令码ClassPool classPool = new ClassPool();// 添加classLoaderclassPool.appendSystemPath();// 1.创建一个类CtClass class1 = classPool.makeClass("TestServiceImpl");class1.addInterface(classPool.get(TestService.class.getName()));// 2.创建一个方法CtMethod satHelloMethod = CtNewMethod.make(CtClass.voidType, // void返回值"sayHello", // 方法名new CtClass[]{classPool.get(String.class.getName())}, // 方法参数new CtClass[0], // 异常类型"{System.out.println(\"hello:\"+$1);}", // 方法体内容,$1表示第一个参数class1 // 指定类);class1.addMethod(satHelloMethod);// 3.实例化这个对象Class aClass = classPool.toClass(class1);// 强制转换return (TestService) aClass.newInstance();}public interface TestService {void sayHello(String name);}
}
2、使用javassist实现一个动态代理框架
import javassist.*;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.stream.Collectors;public class Javassist3Demo {public static void main(String[] args) throws Exception {TestService proxy = createProxy(TestService.class, new InvocationHandler() {@Overridepublic Object invoke(String methodName, Object[] args) {// 根据方法判断逻辑if(methodName.equals("sayHello3")) {System.out.println("hello" + args[0]);return "aa";} else {System.out.println("hello2" + args[0]);return "aa2";}}});proxy.sayHello("zhangsan"); // hellozhangsanproxy.sayHello2("zz", 1);System.out.println(proxy.sayHello3("qq"));}static int count = 0;public static <T> T createProxy(Class<T> classInterface, InvocationHandler handler) throws Exception {ClassPool classPool = new ClassPool();classPool.appendSystemPath();// 1.创建一个类CtClass impl = classPool.makeClass("$Proxy" + count ++);impl.addInterface(classPool.get(classInterface.getName()));// 2.impl类中 添加属性handlerCtField fie = CtField.make("public com.mydemo.Javassast3Demo.InvocationHandler handler=null;", impl);impl.addField(fie);// 有返回值类型和无返回值类型的源码String src = "return ($r)this.handler.invoke(\"%s\", $args);"; // $args获取所有参数String voidSrc = "this.handler.invoke(\"%s\",$args);";for (Method method : classInterface.getMethods()) {CtClass returnType = classPool.get(method.getReturnType().getName());String name = method.getName();CtClass[] parameters = toCtClass(classPool, method.getParameterTypes());CtClass[] errors = toCtClass(classPool, method.getExceptionTypes());// 2.创建一个方法CtMethod newMethod = CtNewMethod.make(returnType, // 返回值name, // 方法名parameters, // 方法参数errors, // 异常类型method.getReturnType().equals(Void.class) ? String.format(voidSrc, method.getName()) : String.format(src, method.getName()), // 方法体内容impl // 指定类);impl.addMethod(newMethod);}// 生成字节码(辅助学习用)//byte[] bytes = impl.toBytecode();//Files.write(Paths.get(System.getProperty("user.dir") + "/target/" + impl.getName() + ".class"), bytes);// 3.实例化这个对象Class aClass = classPool.toClass(impl);T t = (T) aClass.newInstance();aClass.getField("handler").set(t, handler); // 初始化赋值// 强制转换return t;}private static CtClass[] toCtClass(ClassPool pool, Class[] classes) {return Arrays.stream(classes).map(c -> {try {return pool.get(c.getName());} catch (NotFoundException e) {throw new RuntimeException(e);}}).collect(Collectors.toList()).toArray(new CtClass[0]);}public interface InvocationHandler {Object invoke(String methodName, Object args[]);}public class InvocationHandlerImpl implements InvocationHandler {@Overridepublic Object invoke(String methodName, Object[] args) {System.out.println("hello");return null;}}public interface TestService {void sayHello(String name);void sayHello2(String name, Integer id);String sayHello3(String name);}
}
这样,我们只要实现InvocationHandler 接口,就可以自定义代理类的核心逻辑了,我们对生成的代理类进行反编译:
public class $Proxy0 implements TestService {public InvocationHandler handler = null;public String sayHello3(String var1) {return (String)this.handler.invoke("sayHello3", new Object[]{var1});}public void sayHello(String var1) {this.handler.invoke("sayHello", new Object[]{var1});}public void sayHello2(String var1, Integer var2) {this.handler.invoke("sayHello2", new Object[]{var1, var2});}public $Proxy0() {}
}
实现了我们的接口,并且重写了接口的所有方法,最终执行的是InvocationHandler的invoke方法。
这也正是JDK动态代理的基本思想。
二、JDK动态代理
1、编码实现
public interface EchoService {String echo(String message) throws NullPointerException;
}public class DefaultEchoService implements EchoService {@Overridepublic String echo(String message) {return "[ECHO] " + message;}
}import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;/*** JDK动态代理实例*/
public class JDKDynamicProxyDemo {public static void main(String[] args) {ClassLoader classLoader = Thread.currentThread().getContextClassLoader();// 真实的对象DefaultEchoService realObj = new DefaultEchoService();// 代理的对象Object proxy = Proxy.newProxyInstance(classLoader, new Class[]{EchoService.class}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("动态前置");Object obj = null;if (EchoService.class.isAssignableFrom(method.getDeclaringClass())) {// 执行真实方法obj = method.invoke(realObj, args);}System.out.println("动态后置");return obj;}});EchoService echoService = (EchoService) proxy;System.out.println(echoService.echo("Hello,World"));}
}
我们发现,使用JDK动态代理,基本逻辑和我们上面使用javassist手写的工具类差不多,只不过JDK动态代理底层是使用更复杂的方式实现的,我们这里取巧,使用javassist实现。
注意!这里需要DefaultEchoService 实现EchoService接口,代理的其实是EchoService接口而不是DefaultEchoService 类。
动态代理对原代码没有侵入性,通常可以动态加载。
2、基本原理
为什么 Proxy.newProxyInstance 会生成新的字节码?
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();Object proxy = Proxy.newProxyInstance(classLoader, new Class[]{EchoService.class}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {return null;}
});System.out.println(proxy.getClass());// com.sun.proxy.$Proxy0Object proxy2 = Proxy.newProxyInstance(classLoader, new Class[]{Comparable.class}, (proxy1, method, args1) -> {return null;});System.out.println(proxy2.getClass());// com.sun.proxy.$Proxy1
上面代码我们会发现,Java动态代理每生成一个代理,它的class总是com.sun.proxy包下的$Proxy*,从0开始累加,它是如何实现的呢?
我们来分析一下Proxy的newProxyInstance方法:
// java.lang.reflect.Proxy#newProxyInstance
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException
{Objects.requireNonNull(h);// 对象克隆final Class<?>[] intfs = interfaces.clone();final SecurityManager sm = System.getSecurityManager();if (sm != null) {checkProxyAccess(Reflection.getCallerClass(), loader, intfs);}/** Look up or generate the designated proxy class.*/// 先从缓存获取(见 (1))Class<?> cl = getProxyClass0(loader, intfs);/** Invoke its constructor with the designated invocation handler.*/try {if (sm != null) {checkNewProxyPermission(Reflection.getCallerClass(), cl);}// 获取代理对象的构造方法,带着InvocationHandler参数的构造方法final Constructor<?> cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;if (!Modifier.isPublic(cl.getModifiers())) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {cons.setAccessible(true);return null;}});}// 返回Proxy对象return cons.newInstance(new Object[]{h});} catch (IllegalAccessException|InstantiationException e) {throw new InternalError(e.toString(), e);} catch (InvocationTargetException e) {Throwable t = e.getCause();if (t instanceof RuntimeException) {throw (RuntimeException) t;} else {throw new InternalError(t.toString(), t);}} catch (NoSuchMethodException e) {throw new InternalError(e.toString(), e);}
}
(1)getProxyClass0方法
在getProxyClass0方法中,从proxyClassCache缓存中获取了这个代理类:
// java.lang.reflect.Proxy#getProxyClass0
private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {if (interfaces.length > 65535) {throw new IllegalArgumentException("interface limit exceeded");}// If the proxy class defined by the given loader implementing// the given interfaces exists, this will simply return the cached copy;// otherwise, it will create the proxy class via the ProxyClassFactoryreturn proxyClassCache.get(loader, interfaces);
}
而proxyClassCache在初始化时,自动创建了KeyFactory和ProxyClassFactory
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
ProxyClassFactory的核心方法apply,隐藏着代理接口的创建逻辑:
// java.lang.reflect.Proxy.ProxyClassFactory
private static final class ProxyClassFactoryimplements BiFunction<ClassLoader, Class<?>[], Class<?>>
{// prefix for all proxy class namesprivate static final String proxyClassNamePrefix = "$Proxy";// next number to use for generation of unique proxy class namesprivate static final AtomicLong nextUniqueNumber = new AtomicLong();@Overridepublic Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);for (Class<?> intf : interfaces) { // 遍历我们传入的接口数组/** Verify that the class loader resolves the name of this* interface to the same Class object.*/Class<?> interfaceClass = null;try {// 通过classLoader加载我们的接口interfaceClass = Class.forName(intf.getName(), false, loader);} catch (ClassNotFoundException e) {}if (interfaceClass != intf) {throw new IllegalArgumentException(intf + " is not visible from class loader");}/** Verify that the Class object actually represents an* interface.*/if (!interfaceClass.isInterface()) {// 只能代理接口,非接口直接抛异常throw new IllegalArgumentException(interfaceClass.getName() + " is not an interface");}/** Verify that this interface is not a duplicate.*/if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {throw new IllegalArgumentException("repeated interface: " + interfaceClass.getName());}}String proxyPkg = null; // package to define proxy class inint accessFlags = Modifier.PUBLIC | Modifier.FINAL;/** Record the package of a non-public proxy interface so that the* proxy class will be defined in the same package. Verify that* all non-public proxy interfaces are in the same package.*/for (Class<?> intf : interfaces) {int flags = intf.getModifiers();if (!Modifier.isPublic(flags)) {accessFlags = Modifier.FINAL;String name = intf.getName();int n = name.lastIndexOf('.');String pkg = ((n == -1) ? "" : name.substring(0, n + 1));if (proxyPkg == null) {proxyPkg = pkg;} else if (!pkg.equals(proxyPkg)) {throw new IllegalArgumentException("non-public interfaces from different packages");}}}if (proxyPkg == null) { // 包名就是com.sun.proxy// if no non-public proxy interfaces, use com.sun.proxy packageproxyPkg = ReflectUtil.PROXY_PACKAGE + ".";}/** Choose a name for the proxy class to generate.*/long num = nextUniqueNumber.getAndIncrement();String proxyName = proxyPkg + proxyClassNamePrefix + num; // 依次递增/** Generate the specified proxy class.*/// 代理类生成器,返回字节数组,就是字节码byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);try {// classLoader加载类,是一个native方法,返回一个Class对象return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);} catch (ClassFormatError e) {/** A ClassFormatError here means that (barring bugs in the* proxy class generation code) there was some other* invalid aspect of the arguments supplied to the proxy* class creation (such as virtual machine limitations* exceeded).*/throw new IllegalArgumentException(e.toString());}}
}
(2)总结
vm options参数设置-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true,就可以把生成的代理类的源码保存在com.sun.proxy目录下面,或者用arthas来查看运行中的类信息。
JDK动态代理生成的代理类,我们通过反编译,发现其实是这个样子的:
package com.sun.proxy;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import com.demo.EchoService;public final class $Proxy0
extends Proxy
implements EchoService {private static Method m1;private static Method m3;private static Method m2;private static Method m0;public $Proxy0(InvocationHandler invocationHandler) {super(invocationHandler);}static {try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m3 = Class.forName("com.demo.EchoService").getMethod("echo", Class.forName("java.lang.String"));m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);return;}catch (NoSuchMethodException noSuchMethodException) {throw new NoSuchMethodError(noSuchMethodException.getMessage());}catch (ClassNotFoundException classNotFoundException) {throw new NoClassDefFoundError(classNotFoundException.getMessage());}}public final boolean equals(Object object) {try {return (Boolean)this.h.invoke(this, m1, new Object[]{object});}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final String toString() {try {return (String)this.h.invoke(this, m2, null);}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final int hashCode() {try {return (Integer)this.h.invoke(this, m0, null);}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}public final String echo(String string) throws NullPointerException {try {return (String)this.h.invoke(this, m3, new Object[]{string});}catch (Error | RuntimeException throwable) {throw throwable;}catch (Throwable throwable) {throw new UndeclaredThrowableException(throwable);}}
}
InvocationHandler就是Proxy.newProxyInstance传入的最后一个参数。
当调用代理对象的方法时,会执行InvocationHandler的invoke方法。
注意,JDK生成的代理类的包名不总是com.sun.proxy,只有当接口为Public时是这样的,当接口为非public时,生成的代理类与接口所在包名相同。
写在后面
如果本文对你有帮助,请点赞收藏关注一下吧 ~

相关文章:
深入理解JDK动态代理原理,使用javassist动手写一个动态代理框架
文章目录一、动手实现一个动态代理框架1、初识javassist2、使用javassist实现一个动态代理框架二、JDK动态代理1、编码实现2、基本原理(1)getProxyClass0方法(2)总结写在后面一、动手实现一个动态代理框架 1、初识javassist Jav…...
一、策略模式的使用
1、策略模式定义: 策略模式(Strategy Pattern)定义了一组策略,分别在不同类中封装起来,每种策略都可以根据当前场景相互替换,从而使策略的变化可以独立于操作者。比如我们要去某个地方,会根据距…...
Verilog使用always块实现时序逻辑
这篇文章将讨论 verilog 中一个重要的结构---- always 块(always block)。verilog 中可以实现的数字电路主要分为两类----组合逻辑电路和时序逻辑电路。与组合逻辑电路相反,时序电路电路使用时钟并一定需要触发器等存储元件。因此,…...
面向对象设计模式:行为型模式之迭代器模式
一、迭代器模式,Iterator Pattern aka:Cursor Pattern 1.1 Intent 意图 Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation. 提供一种按顺序访问聚合对象的元素而不公开其基…...
如何快速在企业网盘中找到想要的文件
现在越来越多的企业采用企业网盘来存储文档和资料,而且现在市面上的企业网盘各种各样。在使用企业网盘过程中,很多用户会问到企业网盘中如何快速搜索文件的问题。但是无论是“标签”功能还是普通的“关键词搜索”功能,都是单层级的࿰…...
香橙派5使用NPU加速yolov5的实时视频推理(二)
三、将best.onnx转为RKNN格式 这一步就需要我们进入到Ubuntu20.04系统中了,我的Ubuntu系统中已经下载好了anaconda,使用anaconda的好处就是可以方便的安装一些库,而且还可以利用conda来配置虚拟环境,做到环境与环境之间相互独立。…...
算法练习-二分查找(一)
算法练习-二分查找 1 代码实现 1.1 非递归实现 public int bsearch(int[] a, int n, int value) {int low 0;int high n - 1;while (low < high) {int mid (low high) / 2;if (a[mid] value) {return mid;} else if (a[mid] < value) {low mid 1} else {high …...
通用业务平台设计(五):预警平台建设
前言 在上家公司,随着业务的不断拓展(从支持单个国家单个主体演变成支持多个国家多个主体),对预警的诉求越来越紧迫;如何保障业务的稳定性那?预警可以帮我们提前甄别风险,从而让我们可以在风险来临前将其消灭ÿ…...
Windows openssl-1.1.1d vs2017编译
工具: 1. perl(https://strawberryperl.com/) 2. nasm(https://nasm.us/) 3. openssl源码(https://www.openssl.org/) 可以自己去下载 或者我的网盘提供下载: 链接:…...
【深蓝学院】手写VIO第2章--IMU传感器--笔记
0. 内容 1. 旋转运动学 角速度的推导: 左ω∧\omega^{\wedge}ω∧,而ω\omegaω是在z轴方向运动,θ′[0,0,1]T\theta^{\prime}[0,0,1]^Tθ′[0,0,1]T 两边取模后得到结论: 线速度大小半径 * 角速度大小 其中,对旋转矩…...
网络基础(二)之HTTP与HTTPS
应用层 再谈 "协议" 协议是一种 "约定". socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢? 为什么要转换呢? 如果我们将struct message里面的信息…...
Python每日一练(20230306)
目录 1. 翻转二叉树 ★★ 2. 最长公共前缀 ★★ 3. 2的幂 ★ 1. 翻转二叉树 翻转一棵二叉树。 示例 1: 输入: 4/ \2 7/ \ / \ 1 3 6 9 输出: 4/ \7 2/ \ / \ 9 6 3 1示例 2: 输入: 1…...
C/C++每日一练(20230305)
目录 1. 整数分解 ☆ 2. 二叉树的最小深度 ★★ 3. 找x ★★ 1. 整数分解 输入一个正整数,将其按7进制位分解为各乘式的累加和。 示例 1: 输入:49 输出:497^2示例 2: 输入:720 输出:720…...
SAS字典的应用
数据字典中常用信息检索DICTIONARY.COLUMNS、DICTIONARY.TABLES以及DICTIONARY.MEMBERS等字典表的内容。在编程实践中,如何以SAS字典表来提高效率。 1、DICTIONARY.COLUMNS 对于当前SAS任务的全部数据集,表格DICTIONARY.COLUMNS包含了诸如变量的名称、类…...
Mysql中的函数和触发器
函数函数是什么?多用于查询语句,实现了某种功能;用途与存储过程不同,但语法是类似的;函数语法create function 函数名([参数列表]) returns 数据类型 begin DECLARE 变量; sql 语句; return 值; end; 设置函…...
分布式架构之(Zookeeper原理)
Zookeeper是一个典型的分布式数据一致性的结局方案,分布式应用程序可以基于它实现注入数据发布、订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能, Zookeeper可以保证如下分布式一致性特性: 顺…...
Java框架学习 | MyBatis
问题导向学习MyBatis 为什么要有MyBatis框架? 避免Java开发者直接使用 JDBC重复做数据库操作,同时更便捷地实现想要的数据库相关功能,让Java专注于开发业务。 MyBatis框架如何实现该目的? MyBatis是半自动化持久层ORM框架&#x…...
Cookie+Session详解
文章目录批量删除会话技术简介CookieCookie 查看Cookie 的删除Cookie 使用页面获取 cookie 信息cookie 特点Sessionsession 的使用Session 登录权限验证过滤器简介过滤器的使用WebFilter 注解过滤放行登录权限验证批量删除 servlet 类 dao 层 会话技术 简介 在计算机领域…...
CAPL脚本要注意区分elcount和strlen求数组长度的区别,不然要吃大亏
🍅 我是蚂蚁小兵,专注于车载诊断领域,尤其擅长于对CANoe工具的使用🍅 寻找组织 ,答疑解惑,摸鱼聊天,博客源码,点击加入👉【相亲相爱一家人】🍅 玩转CANoe&…...
CSS常用选择器
目录 1.CSS是什么 2.CSS的三种写法 2.1内部样式 2.2内联样式 2.3外部样式 3.CSS选择器 3.1标签选择器 3.2类选择器(更好的选择) 3.3ID选择器 3.4后代选择器 3.5子选择器 3.6并集选择器 3.7伪类选择器(复合选择器的特殊用法) 1.CSS是什么 CSS全称Cascding Style Sh…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...
微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...
UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
关键领域软件测试的突围之路:如何破解安全与效率的平衡难题
在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件,这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下,实现高效测试与快速迭代?这一命题正考验着…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...
NPOI Excel用OLE对象的形式插入文件附件以及插入图片
static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...
关于uniapp展示PDF的解决方案
在 UniApp 的 H5 环境中使用 pdf-vue3 组件可以实现完整的 PDF 预览功能。以下是详细实现步骤和注意事项: 一、安装依赖 安装 pdf-vue3 和 PDF.js 核心库: npm install pdf-vue3 pdfjs-dist二、基本使用示例 <template><view class"con…...
STM32---外部32.768K晶振(LSE)无法起振问题
晶振是否起振主要就检查两个1、晶振与MCU是否兼容;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容(CL)与匹配电容(CL1、CL2)的关系 2. 如何选择 CL1 和 CL…...
LangFlow技术架构分析
🔧 LangFlow 的可视化技术栈 前端节点编辑器 底层框架:基于 (一个现代化的 React 节点绘图库) 功能: 拖拽式构建 LangGraph 状态机 实时连线定义节点依赖关系 可视化调试循环和分支逻辑 与 LangGraph 的深…...
