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

插件化开发入门

一、背景

顾名思义,插件化开发就是将某个功能代码封装为一个插件模块,通过插件中心的配置来下载、激活、禁用、或者卸载,主程序无需再次重启即可获取新的功能,从而实现快速集成。当然,实现这样的效果,必须遵守一些插件接口的标准,不能与已有的功能冲突。目前能支持插件化开发的成熟框架很多,但本文仅从思路的实现角度,从0到1实现简单的插件化开发框架。

二、实现思路

思路:定义插件接口 -> 实现插件接口 -> 通过反射机制加载插件 -> 调用插件方法。

开发语言:支持反射机制的所有高级语言均可实现插件式开发,或有 FFI 调用 Native 函数的编程语言。

三、Java 通过反射机制实现插件化开发

1、创建插件接口

定义插件接口:一个执行方法

package service;/*** 通用插件接口** @author yushanma* @since 2023/3/5 16:36*/
public interface IPluginService {/*** 执行插件*/public void run();
}

2、实现插件接口

package impl;
import service.IPluginService;/*** 打印插件** @author yushanma* @since 2023/3/5 16:37*/
public class MyPrinterPlugin implements IPluginService {@Overridepublic void run() {System.out.println("执行插件方法...");}
}

3、插件中心

管理与加载插件。

Step 1、插件实体类封装

package entity;import lombok.Data;/*** 插件实体类** @author yushanma* @since 2023/3/5 16:44*/
@Data
public class PluginEntity {/*** 插件名*/private String pluginName;/*** 插件路径*/private String jarPath;/*** 字节码名字*/private String className;
}

需要获取插件名、插件实现的Jar包路径、字节码路径

Step 2、通过反射机制实现插件实例化

package loader;import entity.PluginEntity;
import exception.PluginException;
import lombok.Data;
import lombok.NoArgsConstructor;
import service.IPluginService;import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 插件管理器** @author yushanma* @since 2023/3/5 16:44*/
@Data
@NoArgsConstructor
public class PluginManager {private Map<String, Class<?>> clazzMap = new HashMap<>();public PluginManager(List<PluginEntity> plugins) throws PluginException {initPlugins(plugins);}public void initPlugin(PluginEntity plugin) throws PluginException {try {//URL url = new URL("file:" + plugin.getJarPath());URL url = new File(plugin.getJarPath()).toURI().toURL();URLClassLoader classLoader = new URLClassLoader(new URL[]{url});Class<?> clazz = classLoader.loadClass(plugin.getClassName());clazzMap.put(plugin.getClassName(), clazz);} catch (Exception e) {throw new PluginException("plugin " + plugin.getPluginName() + " init error: >>> " + e.getMessage());}}public void initPlugins(List<PluginEntity> plugins) throws PluginException {for (PluginEntity plugin : plugins) {initPlugin(plugin);}}public IPluginService getInstance(String className) throws PluginException {Class<?> clazz = clazzMap.get(className);Object instance = null;try {instance = clazz.newInstance();} catch (Exception e) {throw new PluginException("plugin " + className + " instantiate error," + e.getMessage());}return (IPluginService) instance;}
}

Step 3、通过 XML 文件来配置管理插件

        <dependency><groupId>org.dom4j</groupId><artifactId>dom4j</artifactId><version>2.1.1</version></dependency>
package conf;import entity.PluginEntity;
import exception.PluginException;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;import java.io.File;
import java.util.ArrayList;
import java.util.List;/*** 解析 XML 插件配置** @author yushanma* @since 2023/3/5 16:44*/
public class PluginXmlParser {public static List<PluginEntity> getPluginList() throws PluginException {List<PluginEntity> list = new ArrayList<>();SAXReader saxReader = new SAXReader();Document document = null;try {document = saxReader.read(new File("src/main/resources/plugin.xml"));} catch (Exception e) {throw new PluginException("read plugin.xml error," + e.getMessage());}Element root = document.getRootElement();List<?> plugins = root.elements("plugin");for (Object pluginObj : plugins) {Element pluginEle = (Element) pluginObj;PluginEntity plugin = new PluginEntity();plugin.setPluginName(pluginEle.elementText("name"));plugin.setJarPath(pluginEle.elementText("jar"));plugin.setClassName(pluginEle.elementText("class"));list.add(plugin);}return list;}}
<!-- plugin.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<plugins><plugin><name>测试插件</name><jar>plugins/PrinterPlugin-1.0-SNAPSHOT.jar</jar><class>impl.MyPrinterPlugin</class></plugin><plugin><name>测试插件</name><jar>plugins/PrinterPlugin-1.0-SNAPSHOT.jar</jar><class>impl.MyPrinterPlugin</class></plugin>
</plugins>

Step 4、解析 XML 文件并加载插件

package loader;import conf.PluginXmlParser;
import entity.PluginEntity;
import exception.PluginException;
import service.IPluginService;import java.util.List;/*** 插件加载器** @author yushanma* @since 2023/3/5 16:44*/
public class PluginLoader {public void run() throws PluginException {// 从配置文件加载插件List<PluginEntity> pluginList = PluginXmlParser.getPluginList();PluginManager pluginManager = new PluginManager(pluginList);for (PluginEntity plugin : pluginList) {IPluginService pluginService = pluginManager.getInstance(plugin.getClassName());System.out.println("开始执行[" + plugin.getPluginName() + "]插件...");// 调用插件pluginService.run();System.out.println("[" + plugin.getPluginName() + "]插件执行完成");}// 动态加载插件
//        PluginEntity plugin = new PluginEntity();
//        plugin.setPluginName("");
//        plugin.setJarPath("");
//        plugin.setClassName("");
//        pluginManager.initPlugin(plugin);
//        IPluginService pluginService = pluginManager.getInstance("");
//        pluginService.run();}
}

4、测试效果

import exception.PluginException;
import loader.PluginLoader;/*** desc** @author yushanma* @since 2023/3/5 16:44*/
public class DemoMain {public static void main(String[] args) throws PluginException {PluginLoader loader = new PluginLoader();loader.run();}
}

四、Rust 通过 libloader 库实现插件化开发

通过 libloader 库可以调用动态链接库函数,需要 FFI 支持。

Step 1、创建 lib

cargo new --lib mydll
// 有参数没有返回值
#[no_mangle]
pub fn println(str: &str) {println!("{}", str);
}// 有参数有返回值
#[no_mangle]
pub fn add(a: usize, b: usize) -> usize {a + b
}// 没有参数没有返回值
#[no_mangle]
pub fn print_hello() {println!("Hello");
}// 字符串类型
#[no_mangle]
pub fn return_str(s1: &str) -> &str{s1
}

Step 2、toml 配置编译类型

[package]
name = "mydll"
version = "0.1.0"
edition = "2021"# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html[dependencies]# rlib:Rust库,这是cargo new默认的种类,只能被Rust调用;
# dylib:Rust规范的动态链接库,windows上编译成.dll,linux上编译成.so,也只能被Rust调用;
# cdylib:满足C语言规范的动态链接库,windows上编译成.dll,linux上编译成.so,可以被其他语言调用
# staticlib:静态库,windows上编译成.lib,linux上编译成.a,可以被其他语言调用[lib]
crate-type = ["cdylib"]

Step 3、编译为 dll

cargo build

可以看到,所有的函数都被正常导出,具体原理请参考:https://fasterthanli.me/articles/so-you-want-to-live-reload-rust

Step 4、动态加载 dll

use cstr::cstr;
use libloader::*;
use std::{ffi::CStr,os::raw::c_char};fn main() {get_libfn!("dll/mydll.dll", "println", println, (), s: &str);println("你好");get_libfn!("dll/mydll.dll", "add", add, usize, a: usize, b: usize);println!(" 1 + 2 = {}", add(1, 2));get_libfn!("dll/mydll.dll", "print_hello", print_hello, bool);print_hello();get_libfn!("dll/mydll.dll","return_str", return_str,*const c_char, s: *const c_char);let str = unsafe { CStr::from_ptr(return_str(cstr!("你好 ").as_ptr())) };print!("out {}", str.to_str().unwrap());
}

五、C# 通过反射机制实现插件化开发

Step 1、定义插件接口

namespace PluginInterface
{public interface IPlugin{// 获取插件名字public string GetName();// 获取插件所提供的功能列表public string[] GetFunction();// 执行插件某个功能public bool Execute(string fn);}}

Step 2、实现插件接口

using PluginInterface;
using System;
using System.Linq;namespace MyPlugin
{public class PrinterPlugin : IPlugin{private static readonly string PLUGIN_NAME = "PrinterPlugin";// 获取插件名字public string GetName(){return PLUGIN_NAME;}// 获取插件所提供的功能列表public string[] GetFunction(){return PrinterFunc.FuncDics.Keys.ToArray();}// 执行插件某个功能public bool Execute(string fn){return PrinterFunc.Run(fn);}// 传参功能public static object PrintLabel(string sn){Console.WriteLine($"打印标签{sn}...DONE");return true;}}
}
using System;
using System.Collections.Generic;namespace MyPlugin
{// 封装打印机支持的功能internal class PrinterFunc{// 功能字典public static Dictionary<string, Func<bool>> FuncDics = new Dictionary<string, Func<bool>>{{"PrintPhoto",PrintPhoto },{"PrintDoc",PrintDoc }};// 执行某个功能public static bool Run(string name){if (!FuncDics.ContainsKey(name)){return false;}return (bool)FuncDics[name].Invoke();}// 打印照片public static bool PrintPhoto(){Console.WriteLine("打印照片...DONE");return true;}// 打印文档public static bool PrintDoc(){Console.WriteLine("打印文档...DONE");return true;}}}

Step 3、通过反射实例化插件

using PluginInterface;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;namespace CLI.Loader
{public class PluginLoader{// 初始化时加载插件public PluginLoader(){LoadPlugin();}public Dictionary<string, IPlugin> ListName = new Dictionary<string, IPlugin>();// 加载所有插件public void LoadPlugin(){try{// 清除所有插件缓存ListName.Clear();// 插件文件夹string fileName = "D:\\AwsomeWorkSpace\\CLI\\Plugins\\net5.0\\";// 获取所有插件文件DirectoryInfo info = new DirectoryInfo(fileName);FileInfo[] files = info.GetFiles();foreach (FileInfo file in files){if (!file.FullName.EndsWith(".dll")){continue;}// 通过反射机制创建插件实例Assembly assembly = Assembly.LoadFile(file.FullName);Type[] types = assembly.GetTypes();foreach (Type type in types){// 如果某些类实现了预定义的插件接口,则认为该类适配与主程序(是主程序的插件)if (type.GetInterface("IPlugin") != null){// 创建该类实例IPlugin plugin = assembly.CreateInstance(type.FullName) as IPlugin;if (plugin == null){throw new Exception("插件错误");}ListName.Add(plugin.GetName(), plugin);// 调用插件的某个传参方法MethodInfo printLabel = type.GetMethod("PrintLabel");object res = printLabel.Invoke(plugin, parameters: new object[] { "HQ31122222222222" });Console.WriteLine(res?.ToString());// 调用插件内部的 Execute 方法MethodInfo execute = type.GetMethod("Execute");res = execute.Invoke(plugin, parameters: new object[] { "PrintPhoto" });Console.WriteLine(res?.ToString());res = execute.Invoke(plugin, parameters: new object[] { "PrintDoc" });Console.WriteLine(res?.ToString());}}}}catch (Exception e){Console.WriteLine(e.Message);}}// 插件启动public void Start(){Console.WriteLine("==== 插件中心 ====");Console.WriteLine("1--加载插件列表");Console.WriteLine("2--重新刷新插件");int switchVal = int.Parse(Console.ReadLine());switch (switchVal){case 1:GetPluginList();break;case 2:LoadPlugin();break; ;}}// 加载插件列表public void GetPluginList(){Console.WriteLine("--------插件列表--------");foreach (var VARIABLE in ListName.Keys){Console.WriteLine($"----{VARIABLE}");}Console.WriteLine("--------请输入插件名--------");GetPluginFunc(Console.ReadLine());}// 加载插件功能public void GetPluginFunc(string pluginName){if (!ListName.ContainsKey(pluginName)){return;}IPlugin plugin = ListName[pluginName];string[] funcList = plugin.GetFunction();for (int i = 0; i < funcList.Length; i++){Console.WriteLine(funcList[i]);plugin.Execute(funcList[i]);}}}
}

ok,可以看到,插件化开发的实现并不复杂,但是其中用到的反射机制会消耗部分性能,并且 dll 也会存在一些逆向工程或者反向注入等信安问题,需要谨慎使用。当然,框架的完善更是任重道远的过程。

六、.NET 6/7 导出非托管函数能力

环境:Visual Studio 2022 / .NET7

参考:https://github.com/dotnet/runtime/tree/main/src/coreclr/nativeaot/docs

Step 1、创建类库项目

dotnet new classlib -o mydll -f net6.0

Step 2、配置 AOT Native

<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net7.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable><PublishAot>true</PublishAot></PropertyGroup></Project>

Step 3、导出非托管函数

using System.Runtime.InteropServices;
using Seagull.BarTender.Print;namespace ClassLibrary1
{public class Class1{// 无参数有返回值[UnmanagedCallersOnly(EntryPoint = "IsOk")]public static bool IsOk(){return true;}// 有参数无返回值[UnmanagedCallersOnly(EntryPoint = "MyPrinter")]public static void MyPrinter(IntPtr pString){try{if (pString != IntPtr.Zero){string str = new(Marshal.PtrToStringAnsi(pString));Console.WriteLine(str);}}catch (Exception e){Console.WriteLine(">>> Exception " + e.Message);}}// 有参数有返回值[UnmanagedCallersOnly(EntryPoint = "MyConcat")]public static IntPtr MyConcat(IntPtr pString1, IntPtr pString2){string concat = "";try{if (pString1 != IntPtr.Zero && pString2 != IntPtr.Zero){string str1 = new(Marshal.PtrToStringAnsi(pString1));string str2 = new(Marshal.PtrToStringAnsi(pString2));concat = string.Concat(str1, str2);}}catch (Exception e){concat = e.Message;}return Marshal.StringToHGlobalAnsi(concat);}// 无参数无返回值[UnmanagedCallersOnly(EntryPoint = "PrintHello")]public static void PrintHello(){Console.WriteLine(">>> Hello");}}
}

Step 4、查看导出结果

dotnet publish /p:NativeLib=Shared /p:SelfContained=true -r win-x64 -c release

可以看到 native 、publish 文件夹,里面的 dll 文件

函数正常导出,最后一个是默认导出的函数。

相关文章:

插件化开发入门

一、背景顾名思义&#xff0c;插件化开发就是将某个功能代码封装为一个插件模块&#xff0c;通过插件中心的配置来下载、激活、禁用、或者卸载&#xff0c;主程序无需再次重启即可获取新的功能&#xff0c;从而实现快速集成。当然&#xff0c;实现这样的效果&#xff0c;必须遵…...

tftp、nfs 服务器环境搭建

目录 一、认识 tftp、nfs 1、什么是 tftp&#xff1f; 2、什么是 nfs&#xff1f; 3、tftp 和 nfs 的区别 二、tftp的安装 1、安装 tftp 服务端 2、配置 tftp 3、启动 tftp 服务 三、nfs 的安装 1、安装 nfs 服务端 2、配置 nfs 3、启动 nfs 服务 一、认识 tftp、…...

汇编系列03-不借助操作系统输出Hello World

每天进步一点点&#xff0c;加油&#xff01; 上一节&#xff0c;我们通过汇编指令&#xff0c;借助操作系统的系统调用实现了向标准输出打印Hello world。这一节我们打算绕过操作系统&#xff0c;直接在显示屏幕上打印Hello world。 计算机的启动过程 当我们给计算机加电启…...

TPU编程竞赛系列|算能赛道冠军SO-FAST团队获第十届CCF BDCI总决赛特等奖!

近日&#xff0c;第十届中国计算机学会&#xff08;CCF&#xff09;大数据与计算智能大赛总决赛暨颁奖典礼在苏州顺利落幕&#xff0c;算能赛道的冠军队伍SO-FAST从2万余支队伍中脱颖而出&#xff0c;获得了所有赛道综合评比特等奖&#xff01; 本届CCF大赛吸引了来自全国的2万…...

【C++】AVL树,平衡二叉树详细解析

文章目录前言1.AVL树的概念2.AVL树节点的定义3.AVL树的插入4.AVL树的旋转左单旋右单旋左右双旋右左双旋AVL树的验证AVL树的删除AVL树的性能前言 前面对map/multimap/set/multiset进行了简单的介绍&#xff0c;在其文档介绍中发现&#xff0c;这几个容器有个共同点是&#xff1…...

C/C++开发,无可避免的多线程(篇四).线程与函数的奇妙碰撞

一、函数、函数指针及函数对象 1.1 函数 函数&#xff08;function&#xff09;是把一个语句序列&#xff08;函数体, function body&#xff09;关联到一个名字和零或更多个函数形参&#xff08;function parameter&#xff09;的列表的 C 实体&#xff0c;可以通过返回或者抛…...

elisp简单实例: taglist

从vim 转到emacs 下,一直为缺少vim 中的tablist 插件而感到失落. 从网上得到的一个emacs中的taglist, 它的功能很简陋,而且没有任何说明, 把它做为elisp的简单实例,供初学者入门倒不错,我给它加了很多注释,帮助理解, 说实话,感觉这百行代码还是挺有深度的,慢慢体会,调试才会有收…...

Azure AI基础到实战(C#2022)-认知服务(3)

目录 OpenFileDialog 类上一节代码的API剖析ComputerVisionClientExtensions.ReadAsync MethodReadHeaders ClassReadHeaders.OperationLocation Property探索ReadHeaders加上调试代码可用于 Azure 认知服务的身份验证标头使用单服务订阅密钥进行身份验证使用多服务订阅密钥进行…...

aws apigateway 使用restapi集成lambda

参考资料 代理集成&#xff0c;https://docs.aws.amazon.com/zh_cn/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html非代理集成&#xff0c;https://docs.aws.amazon.com/zh_cn/apigateway/latest/developerguide/getting-started-…...

HTML基础

HTML 基础 文章目录HTML 基础列表标签无序列表有序列表自定义列表表格标签表格基本标签表格基本结构表格完整结构&#xff1a;合并行和合并列表单标签input 系列标签属性标签text 标签radio 标签 单选框file 标签 文件选择button 标签 按钮input系列标签总结button按钮标签sele…...

ThreadPoolExecutor参数 keepAliveTime allowCoreThreadTimeOut

/*** Timeout in nanoseconds for idle threads waiting for work.* Threads use this timeout when there are more than corePoolSize* present or if allowCoreThreadTimeOut. Otherwise they wait* forever for new work.*/ private volatile long keepAliveTime;等待工作的…...

什么是Hibernate框架?

简单介绍&#xff1a;Hibernate框架是当今主流的java持久层框架之一&#xff0c;是一个开放源码的ORM&#xff08;Object Relational Mapping&#xff0c;对象关系映射&#xff09;框架&#xff0c;它对JDBC进行了轻量级的封装&#xff0c;使得JAVA开发人员可以使用面向对象的编…...

指针面试笔试题练习

前言 &#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏: &#x1f354;&#x1f35f;&#x1f32f; c语言进阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f349;本篇简介:>:介绍c语言中有关指针更深层的知识. 金句分享: ✨星光…...

docker(三)仓库的搭建、官方私有仓库的加密和认证

文章目录一、docker仓库二、仓库Registry工作原理三、搭建本地私有仓库四、配置镜像加速器五、私有仓库的加密认证1.非加密下上传拉取2.insecure registry3.仓库加密4.仓库认证一、docker仓库 什么是仓库 Docker 仓库是用来包含镜像的位置&#xff0c;Docker提供一个注册服务器…...

FPGA实现SDI视频编解码 SDI接收发送,提供2套工程源码和技术支持

目录1、前言2、设计思路和框架SDI接收SDI缓存写方式处理SDI缓存读方式处理SDI缓存的目的SDI发送3、工程1详解4、工程2详解5、上板调试验证并演示6、福利&#xff1a;工程代码的获取1、前言 FPGA实现SDI视频编解码目前有两种方案&#xff1a; 一是使用专用编解码芯片&#xff0…...

Android 基础知识4-3.5 RadioButton(单选按钮)Checkbox(复选框)详解

一、RadioButton&#xff08;单选按钮&#xff09; 1.1、简介 RadioButton表示单选按钮&#xff0c;是button的子类&#xff0c;每一个按钮都有选择和未选中两种状态&#xff0c;经常与RadioGroup一起使用&#xff0c;否则不能实现其单选功能。RadioGroup继承自LinearLayout&a…...

用代码实现解析解的方式求解_梯度下降法思路_导函数有什么用_接23节---人工智能工作笔记0026

这里24节,25节,介绍了一下人工智能高等数学要学习的一些内容,初步了解了一下,微积分中用到的知识~微分~以及导数这里... 然后接着23节,我们还是继续,走人工智能的主线,先把整体的人工智能的内容学习一遍,然后再去回去看数学知识更有目的性. 然后首先来回顾一下,这里机器学习,其…...

大数据ETL开发之图解Kettle工具

详细笔记参考&#xff1a;https://blog.csdn.net/yuan2019035055/article/details/120409547以下只是简单记录一下我学习过程中的心得3.1.5 JSON输入JSONPath 类似于 XPath 在 xml 文档中的定位&#xff0c;JsonPath 表达式通常是用来路径检索或设置Json的。其表达式可以接受“…...

docker-容器数据卷

Docker挂载主机目录访问如果出现cannot open directory.:Permission denied 解决办法:在挂载目录后多加一个--privileged=true参数即可; 一、介绍 卷就是目录或文件,存在于一个或多个容器中,由docker挂载到容器,但不属于联合文件系统,因此能绕过Union Fil…...

【C++】类和对象补充知识点

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;C的学习之路 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录前言一、再谈构造函数1.1 构造函数体赋…...

SCAU期末笔记 - 数据分析与数据挖掘题库解析

这门怎么题库答案不全啊日 来简单学一下子来 一、选择题&#xff08;可多选&#xff09; 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘&#xff1a;专注于发现数据中…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

ElasticSearch搜索引擎之倒排索引及其底层算法

文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...

免费PDF转图片工具

免费PDF转图片工具 一款简单易用的PDF转图片工具&#xff0c;可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件&#xff0c;也不需要在线上传文件&#xff0c;保护您的隐私。 工具截图 主要特点 &#x1f680; 快速转换&#xff1a;本地转换&#xff0c;无需等待上…...

MFC 抛体运动模拟:常见问题解决与界面美化

在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...

华为OD机考-机房布局

import java.util.*;public class DemoTest5 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseSystem.out.println(solve(in.nextLine()));}}priv…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)

引言 在人工智能飞速发展的今天&#xff0c;大语言模型&#xff08;Large Language Models, LLMs&#xff09;已成为技术领域的焦点。从智能写作到代码生成&#xff0c;LLM 的应用场景不断扩展&#xff0c;深刻改变了我们的工作和生活方式。然而&#xff0c;理解这些模型的内部…...

uniapp 字符包含的相关方法

在uniapp中&#xff0c;如果你想检查一个字符串是否包含另一个子字符串&#xff0c;你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的&#xff0c;但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...

python爬虫——气象数据爬取

一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用&#xff1a; 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests&#xff1a;发送 …...

从面试角度回答Android中ContentProvider启动原理

Android中ContentProvider原理的面试角度解析&#xff0c;分为​​已启动​​和​​未启动​​两种场景&#xff1a; 一、ContentProvider已启动的情况 1. ​​核心流程​​ ​​触发条件​​&#xff1a;当其他组件&#xff08;如Activity、Service&#xff09;通过ContentR…...