首页 文章 精选 留言 我的

精选列表

搜索[增量式],共10000篇文章
优秀的个人博客,低调大师

AR空间音频能力,打造沉浸式声音体验

随着元宇宙的兴起,3D虚拟现实广泛引用,让数字化信息和现实世界融合,目前大家的目光主要聚焦于视觉交互层面,为了在虚拟环境中更好的再现真实世界的三维空间体验,引入听觉层面必不可少,空间音频孕育而生。 空间音频是一种音频体验,会让用户更容易体验到声音的立体感和深度,将环绕声更准确地定位在合适的方位,让用户在生理上获得更身临其境的环绕效果。目前主要应用在AR、VR、语音视频聊天室、在线会议、在线游戏、虚拟演唱会等,以及对象音频制作等场景。 针对开发者的需求,华为HMS Core AR Engine XR Kit在AR展示(ARSceneView)中新增空间音频能力,在已经加载的模型上添加音频后,XR Kit内部根据设备和模型的空间位置变化,模拟真实世界中音频的发声,提升听觉体验。不佩戴耳机,只能感受音频的声音大小变化,佩戴耳机后,可以同时感受音频的声音大小及方位变化,空间音频效果体验更佳。 技术原理 利用听音辨位的双耳效应原理,依靠双耳间的音量差、时间差和音色差判别声音方位的效应,也就是说空间中音源的声波从不同的方向传播到左右耳的路径不同、时间不同,以及耳廓中的折射、反射、吸收等,从而导致音量、音色、延迟等在左右耳会产生不同的变化,这些声波变化过程就是声波的空间传递函数。 空间音频是预先测量记录在模拟的消音室环境中全方位声音传到人耳的变化过程HRTF(Head Related Transfer Functions头部相对传输函数),然后上层传入的声源和听者的空间信息(三维位置信息和三维旋转信息),以计算出听者和声源的相对空间信息,并根据HRTF数据库得到当前相对空间信息下的两个声道的HRTF响应脉冲函数,与当前双声道音频数据进行卷积计算渲染出左右耳的声音直传双耳,从而可以实现听音辨位。 华为HMS Core AR Engine XR Kit未来将根据开发者的述求,逐步开放空间音频引擎的更多能力。针对开放几何空间,将开放多普勒效应、距离衰减、回声等技术,提升在开放空间中相对运动时的空间音效体验;针对密闭几何空间,将引入房间声学,例如实现遮挡、透射、反射、吸收、回声等基础音效算法,更好的实现密闭几何空间的3D声效,还原真实听感。 开发步骤 1. 开发环境要求: 在华为终端设备上的应用市场下载XRKit服务端APK(需在华为应用市场,搜索“华为XRKit”)并安装到终端设备。 JDK 1.8.211及以上; 安装Android Studio 3.0及以上: minSdkVersion 27及以上 targetSdkVersion 29(推荐) compileSdkVersion 29(推荐) Gradle 6.1.1及以上(推荐) 测试应用的设备:参见AREngine特性软硬件依赖表。 如果同时使用多个HMS Core的服务,则需要使用各个Kit对应的最大值。 2. 开发准备 在开发应用前需要在华为开发者联盟网站上注册成为开发者并完成实名认证,具体方法请参见帐号注册认证。 参见创建项目和在项目下创建应用完成应用的创建,配置如下: “选择平台”:选择“Android”。 “支持设备”:选择“手机”。 “应用分类”:选择“应用”或“游戏”。 华为提供了Maven仓集成方式的XRKit SDK包,在开始开发前,您需要将XRKit SDK集成到您的开发环境中,特别注意的是XRKit的Android版本要是1.6.0.0及其以上。 Android Studio的代码库配置在Gradle插件7.0以下版本、7.0版本和7.1及以上版本有所不同。请根据您当前的Gradle插件版本,选择对应的配置过程。 以7.1版本为例: 打开Android Studio项目级“build.gradle”文件,添加Maven代码库; 在“buildscript > repositories”中配置HMS Core SDK的Maven仓地址: buildscript { repositories { google() jcenter() maven {url "https://developer.huawei.com/repo/" } } } 打开项目级“settings.gradle”文件,配置HMS Core SDK的Maven仓地址。 pluginManagement { repositories { repositories { google() jcenter() maven {url "https://developer.huawei.com/repo/" } } } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { repositories { google() jcenter() maven {url "https://developer.huawei.com/repo/" } } } } 在项目应用级的“build.gradle”文件中“dependencies”添加编译依赖。 dependencies { implementation 'com.huawei.hms:xrkitsdk:{version}' } 3. 应用开发 3.1 模型文件制作 在使用XRKit能力之前,您需要准备模型文件,用于在虚拟现实中展示,模型文件制作规范请参见模型文件规范说明。 3.2 运行前验证 检查当前设备是否安装了XRKit,若已经安装则正常运行。具体实现代码如下: if (!XrKitFeatureFactory.isXrKitExist(getApplicationContext())) { Toast.makeText(this, "XRKit is not available ", Toast.LENGTH_SHORT).show(); return false; } 3.3 创建AR场景 使用XRKit特性组件工厂类XrKitFeatureFactory,实例化特性组件管理对象xrKitFeature并设置特性事件监听,特性事件监听用来获取XRKit服务端上报的必要特性事件。xrKitFeature可根据需求创建不同AR场景,本例创建的是AR展示场景(ArSceneView)。 try { if (xrKitFeature == null) { xrKitFeature = XrKitFeatureFactory.createXrKitFeature(getApplicationContext()); } // Register a feature listener. xrKitFeature.setFeatureEventListener(this); // Create arSceneView. sceneView = xrKitFeature.createArSceneView(getApplicationContext()); } catch (XrKitUnavailableServiceApkTooOldException e) { Toast.makeText(this, "XRKit Service is Too Old, Please upgrade!", Toast.LENGTH_SHORT).show(); return false; } 3.4 绑定视图到应用布局 获取当前场景的视图,绑定视图到应用的UI布局当中。 layout.addView(sceneView.getView(), 0, new ConstraintLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 3.5 选择模式 Ar展示场景(ArSceneView)有AR模式和3D模式两种模式,可根据需求选择AR模式或3D模式。 AR模式:即虚实结合,您可以构建能在用户环境中响应的图像(如家具或产品包装)的AR应用。 3D模式:虚拟动画,您可以利用此功能构建展示动画的AR应用。 // AR模式 sceneView.setArMode(true); // 3D模式 sceneView.setArMode(false); 3.6 设置场景 加载虚拟模型、设置3D场景的背景、设置AR场景下平面的可见性。 sceneView.loadModel(GONGCHENGSHI, "gongchengshi"); sceneView.setBackground(IMG_BLACK); sceneView.setPlaneVisible(true); 在activity的onResume方法中调用sceneView的resume方法,启动sceneView场景。 protected void onResume() { Log.d(TAG, "activity resume."); super.onResume(); if (sceneView != null) { sceneView.resume(); } } 3.7 添加空间音频 在模型处添加音频,若此时空间音效引擎未做初始化,会实施初始化的动作。目前只支持MP3和WAV(PCM)格式音频。标识音频的全局唯一ID需要您根据业务需求指定,传入重复的ID,不会覆盖已经添加的音频。添加的音频在设备与模型的距离小于等于5米时生效。 model.attachSoundToModel(String soundId, String soundUrl) // 在模型处添加音频 String soundId, 例如:“Sound_Chariots_Repeat” String soundUrl, 例如: “assets://sounds/chariots.mp3” model.detachSoundFromModel(String soundId) // 从模型中删除音频。 model.playSound(String soundId, int playMode) // 开始播放音频。 playMode有两种模式(参考IModel): int PLAY_MODE_ONCE // 播放一次音频。 int PLAY_MODE_REPEAT // 循环播放音频。 model.pauseSound(String soundId) // 暂停播放音频。 了解更多详情>> 访问华为开发者联盟官网 获取开发指导文档 华为移动服务开源仓库地址:GitHub、Gitee 关注我们,第一时间了解 HMS Core 最新技术资讯~

优秀的个人博客,低调大师

Elixir v1.14 发布,函数式编程语言

Elixir v1.14 已发布。此版本为 Elixir 的调试体验和数据类型检查带来了许多改进。此外还添加了一个新的抽象,以方便进程的分区,称为PartitionSupervisor,以及优化编译时间和错误信息。 另外,Elixir v1.14 是支持 Erlang/OTP 23 的最后一个版本。建议开发者考虑升级到 Erlang/OTP 24 或 Erlang/OTP 25。 dbg Kernel.dbg/2是一个新的宏,有点类似于IO.inspect/2,专门为调试而定制。 调用该宏时,它会打印传递给它的任何值,以及已调试代码本身及其位置。 下面这段代码: # In my_file.exs feature = %{name: :dbg, inspiration: "Rust"} dbg(feature) dbg(Map.put(feature, :in_version, "1.14.0")) 会打印出: $ elixir my_file.exs [my_file.exs:2: (file)] feature #=> %{inspiration: "Rust", name: :dbg} [my_file.exs:3: (file)] Map.put(feature, :in_version, "1.14.0") #=> %{in_version: "1.14.0", inspiration: "Rust", name: :dbg} dbg/2能完成更多任务。它是一个宏,所以可以理解 Elixir 代码_。当开发者将一系列|>管道传递给它时,dbg/2将打印管道每一步的值。 下面这段代码: # In dbg_pipes.exs __ENV__.file |> String.split("/", trim: true) |> List.last() |> File.exists?() |> dbg() 会打印出: $ elixir dbg_pipes.exs [dbg_pipes.exs:5: (file)] __ENV__.file #=> "/home/myuser/dbg_pipes.exs" |> String.split("/", trim: true) #=> ["home", "myuser", "dbg_pipes.exs"] |> List.last() #=> "dbg_pipes.exs" |> File.exists?() #=> true PartitionSupervisor PartitionSupervisor是一个新模块,实现了新的 supervisor 类型。partition supervisor旨在帮助处理成为瓶颈的单个受监督进程的情况。如果该进程的状态可以轻松分区,那么可以使用PartitionSupervisor来监督该进程同​​时运行的多个隔离副本,每个副本都分配了自己的分区。 详情查看 release note。

优秀的个人博客,低调大师

别让低效的任务汇报式早会,拖后腿!

【小剧场】 上午 10:00 - 会议室 - 每日站会 “人到齐了,开始吧。” “任务顺利完成,没遇到什么问题。” “好,下一个吧。” …… “都说完了吗?还有没有补充?” …… “有什么问题,需要协助吗?” …… “都没有吧。好,那今天就先到这。” 如果你在上述小剧场产生了强烈的共鸣,那可能说明,你的站立会出现了大问题! 每日站会即站立会,是Daily Scrum应用最广泛的形式。开发团队每天会利用固定时间的 15 分钟举办内部短会,用于同步项目进展,识别和解决进度障碍。 但是在许多团队中,本应该解决障碍的站立会却变成了障碍本身:会议充斥着机械的工作汇报,对协作提升没有任何帮助;或者会议内容发散拖沓,15 分钟的站立短会经常变成 1 个多小时,成员苦不堪言。 “既然站立会没什么用,为什么不把它取消呢?” 站立会能不能取消?要不要取消?本文将从会议的内容、形式和目的三个方面,解读这个问题。 一、 站立会不要做工作汇报 站立会上有三个众所皆知的问题: 我昨天做了什么(以完成迭代目标)? 我今天打算做什么(以完成迭代目标)? 我(在完成迭代目标过程中)遇到了什么问题、需要什么帮助? 许多人认为,站立会就是在向Scrum Master汇报前一天的工作进度。事实上,Scrum Guides明确指出,Daily Scrum是面向开发者的会议,而不是面向Scrum Master的。 The Daily Scrum is a 15-minute eventfor the Developersof the Scrum Team.\ If the Product Owner or Scrum Master are actively working on items in the Sprint Backlog, they participate as Developers. 站立会的关键角色是开发者。在非必要情况下,Scrum Master可以不参加会议;在会议中,Scrum Master应该起到把控会议方向和解决困难的作用,而不是接受成员的工作汇报。 与过去工具匮乏的时代不同,当代的敏捷团队可以运用各种工具或方法直接、清晰地呈现每位成员的工作任务和完成进度,并且保持信息公开和同步,比如 LigaAI 的「跟进迭代」功能。 因此,「同步迭代进度」的本质,是让任务上下游的成员获取有关迭代事项的最新信息,以便对方能对接下来的工作做出及时、适当的调整。 在站立会上,同步的一定是与他人工作有关的信息,而不是纯粹的「我做了什么」和「我要做什么」,比如: 任务A的工作量比较大,预计在今天上午才能完成,下游的同学请再等等; 任务B存在技术难点,具体是XX问题,需要大家的协助推进; 我今天准备着手解决工作C,需要和下游成员开会讨论。 站立会的第二个会议重点是主动减少障碍,即对工作中遇到的问题主动地进行求助,并寻求解决。 通过回答「遇到了什么困难」,主动地寻求团队其他成员和Scurm Master的协助并解决问题,实现短周期的「自查-修复-优化」,消灭迭代风险,以更快、更好地状态开启新的工作。 在敏捷协作中,切忌埋头苦干。只有所有成员齐心协力地达成迭代目标,提升价值交付的速率,才符合敏捷开发的要求。 二、 Daily Scrum不是站立会 另一个误区是,Daily Scrum就是开站会。 Daily Scrum是将每日工作视为一个小迭代,通过定期发起 15 分钟的集体活动对已完成的工作进行反馈和优化,完成「计划-实践-检查-优化」循环。 而站立会只是实践Daily Scrum的一种形式。在Scrum Guides中,对Daily Scrum的形式说明是这样的: The Developers canselect whatever structure and techniques they want, as long as their Daily Scrum focuses on progress toward the Sprint Goal and produces an actionable plan for the next day of work. 也就是说,开发者可以选择以任何形式完成Daily Scrum,只要能更快地完成迭代目标。 站立会之所以被广泛应用,是因为比起舒适、轻松的坐姿开会,站立姿势天然附加的「不适感」和「紧迫感」更容易让人集中注意力,提高会议效率。但这不意味着「站立会」是Daily Scrum的唯一解。 依旧是老生常谈的道理:条条大路通罗马。只要能够实现信息高效同步,优化协作方式,提升迭代效率,不管Daily Scrum是躺着开、坐着开还是站着开,哪怕是改站立为平板支撑、深蹲、马步、哑铃托举都是可行的。 不拘泥于形式,只做最正确的事。 敏捷开发本就应该竭尽全力,寻求最佳的路径持续改进工作。 三、Daily Scrum不需要每天开会 Daily Scrum将每日工作视为一个小迭代,通过「计划-实践-检查-优化」循环,减少迭代进度的风险,更快地实现迭代目标。 · 敏捷团队为什么要每天开会同步进度? 敏捷开发通过短周期、快速地向用户提供价值交付。在时间紧、任务重的多人协作语境下,团队成员各自认领了组成用户故事的部分子任务,彼此的工作紧密关联。敏捷团队需要通过每日会议,及时同步迭代进度,更好地管理迭代目标。 第二,攻破技术难题需要及时反馈和集思广益。 跨职能的自组织团队和Scrum Master存在,都能帮助成员更高效、快速地解决工作问题,扫清迭代风险。 第三,集体出席会议是早期实现信息同步的最佳实践。 在那个异步/远程协作工具匮乏的时代,发起会议和面对面沟通是保证有效传达和注意力集中最有用的途径。 · Daily Scrum一定要每天开会同步进度吗? 正如前面所说,现在有非常多简单易用的协同工具(比如LigaAI)可以轻松实现团队内的信息共享和同步,在线会议和即时通讯工具等远程办公工具也能随时随地为解决问题提供服务。 因此,在工具的有效帮助下,Daily Scrum可以不用每天开会。换句话说,站立会的频率是可以调整或者取消的。 敏捷团队可以根据任务关联强度、团队工作节奏和进度同步需求等综合判断,调整Daily Scrum的周期,适当地拉长集体反馈和优化的间隔,更好地服务团队。 · 取消每日站会,进度同步该如何实现? 每日站会的目的是通过及时的进度同步和问题反馈,优化协作方式,追求并实现更快的价值交付。在定期的群策群力来临前,敏捷团队可以使用各种方式,实现迭代进度的有效同步。比如: 在LigaAI的规划迭代或者Kanban中同步信息 使用共享表格或文档,下班前完成内容同步 配置全员可见的物理看板,手动完成内容更新 用简单的格式,群发工作日报/周报邮件 建立Done-List,通过群接龙完成进度同步 团队协作没有标准答案,最适合团队的决策就是最正确的决策。 早上开站立会,或者晚上九点开远程会议,亦或者用共享文档代替开会,都没有优劣之分。Daily Scrum只为了同步迭代进度,推进更好的敏捷协作,更快地实现价值交付。 · 如果成员不愿意主动获取/同步进度信息,怎么办? 在《自组织是管理者和成员的双向奔赴》一文中,LigaAI创始人 Ryan 已经做出了解答。 自组织团队非常强调付出。其一,每个人都关心团队目标中未完成的工作,并主动承担任务。其二,成员会观察除自己手头工作外,团队其他人是否有人需要帮助,而不是只在乎自己眼前的“一亩三分地”。 如果成员一直不能履行承诺,或者经过几次培训支持仍然没办法跟上团队的节奏,那管理者就需要做出成员优化。同样的,成员也会主动表示,团队中有成员长期拖后腿,我们需要管理者介入招募能够一起合作的伙伴。 # Liga总结 不在每日站会上做工作汇报。 高效的站立会只传递与他人有关的进度信息,以促进更紧密、更顺畅的合作。 不拘于形式,做最正确的事情。 只要能够提升成员的专注度,高效完成会议目标,提升会议效率,无论是舒服地坐着,或是痛苦地平板支撑,都是可行的。 选择最适合团队的Daily Scrum运行模式。 根据团队规模、任务关联强度、沟通需求等综合判断,选择最适合团队协作方式。 了解更多敏捷开发、项目管理、行业动态等消息,关注我们 LigaAI@oschina或点击LigaAI - 新一代智能研发协作平台,在线申请体验我们的产品。

优秀的个人博客,低调大师

基于SPI的增强式插件框架设计

本文的技术,从双亲委派模型到自定义类加载器,再到基于自定义类加载器实现的类交换,基于Java SPI实现的类交换,最后到基于Java SPI+ Java Agent + Javassist实现的插件框架及框架支持远程插件化,来一步一步的向读者展示所涉及的知识点。 很久之前,为了诊断线上的问题,就想要是能有工具可以在线上出问题的时候,放个诊断包进去马上生效,就能看到线上问题的所在,那该是多么舒服的事情。后来慢慢的切换到java领域后,这种理想也变成了现实,小如IDEA中更改页面就能马上生效,大如利用Althas工具进行线上数据诊断,可谓是信手拈来,极大的方便了开发和诊断。后来深入研究之后,就慢慢的不满足框架本身带来的便利了,造轮子的想法慢慢在脑中挥之不去,这也是本文产生的原因了。接下来,你无需准备任何前置知识,因为我已经为你准备好了ClassLoader甜点,Javassist配菜,JavaAgent高汤,手写插件加载器框架主食,外加SPI知识做调料,且让我们整理餐具,开始这一道颇有点特色的吃播旅程吧。 双亲委派模型 开始前,让我们先聊聊双亲委派这个话题,因为无论是做热部署,还是做字节码增强,甚至于日常的编码,这都是绕不开的一个话题。先看如下图示: 从如上图示,我们可以看到双亲委派模型整体的工作方式,整体讲解如下: 类加载器的findClass(loadClass)被调用 进入App ClassLoader中,先检查缓存中是否存在,如果存在,则直接返回 步骤2中的缓存中不存在,则被代理到父加载器,即Extension ClassLoader 检查Extension ClassLoader缓存中是否存在 步骤4中的缓存中不存在,则被代理到父加载器,即Bootstrap ClassLoader 检查Bootstrap ClassLoader缓存中是否存在 步骤6中的缓存中不存在,则从Bootstrap ClassLoader的类搜索路径下的文件中寻找,一般为rt.jar等,如果找不到,则抛出ClassNotFound Exception Extension ClassLoader会捕捉ClassNotFound错误,然后从Extension ClassLoader的类搜索路径下的文件中寻找,一般为环境变量$JRE_HOME/lib/ext路径下,如果也找不到,则抛出ClassNotFound Exception App ClassLoader会捕捉ClassNotFound错误,然后从App ClassLoader的类搜索路径下的文件中寻找,一般为环境变量$CLASSPATH路径下,如果找到,则将其读入字节数组,如果也找不到,则抛出ClassNotFound Exception。如果找到,则App ClassLoader调用defineClass()方法。 通过上面的整体流程描述,是不是感觉双亲委派机制也不是那么难理解。本质就是先查缓存,缓存中没有就委托给父加载器查询缓存,直至查到Bootstrap加载器,如果Bootstrap加载器在缓存中也找不到,就抛错,然后这个错误再被一层层的捕捉,捕捉到错误后就查自己的类搜索路径,然后层层处理。 自定义ClassLoader 了解了双亲委派机制后,那么如果要实现类的热更换或者是jar的热部署,就不得不涉及到自定义ClassLoader了,实际上其本质依旧是利用ClassLoader的这种双亲委派机制来进行操作的。遵循上面的流程,我们很容易的来实现利用自定义的ClassLoader来实现类的热交换功能: 复制 public class CustomClassLoader extends ClassLoader { //需要该类加载器直接加载的类文件的基目录 private String baseDir; public CustomClassLoader(String baseDir, String[] classes) throws IOException { super(); this.baseDir = baseDir; loadClassByMe(classes); } private void loadClassByMe(String[] classes) throws IOException { for (int i = 0; i < classes.length; i++) { findClass(classes[i]); } } /** * 重写findclass方法 * * 在ClassLoader中,loadClass方法先从缓存中找,缓存中没有,会代理给父类查找,如果父类中也找不到,就会调用此用户实现的findClass方法 * * @param name * @return */ @Override protected Class findClass(String name) { Class clazz = null; StringBuffer stringBuffer = new StringBuffer(baseDir); String className = name.replace('.', File.separatorChar) + ".class"; stringBuffer.append(File.separator + className); File classF = new File(stringBuffer.toString()); try { clazz = instantiateClass(name, new FileInputStream(classF), classF.length()); } catch (IOException e) { e.printStackTrace(); } return clazz; } private Class instantiateClass(String name, InputStream fin, long len) throws IOException { byte[] raw = new byte[(int) len]; fin.read(raw); fin.close(); return defineClass(name, raw, 0, raw.length); } } 这里需要注意的是,在自定义的类加载器中,我们可以覆写findClass,然后利用defineClass加载类并返回。 上面这段代码,我们就实现了一个最简单的自定义类加载器,但是能映射出双亲委派模型呢? 首先点开ClassLoader类,在里面翻到这个方法: 复制 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } 如果对比着双亲委派模型来看,则loadClass方法对应之前提到的步骤1-8,点进去findLoadedClass方法,可以看到底层实现是native的native final Class<?> findLoadedClass0 方法,这个方法会从JVM缓存中进行数据查找。后面的分析方法类似。 而自定义类加载器中的findClass方法,则对应步骤9: 复制 clazz = instantiateClass(name, new FileInputStream(classF), classF.length()); //省略部分逻辑 return defineClass(name, raw, 0, raw.length); 看看,整体是不是很清晰? 自定义类加载器实现类的热交换 写完自定义类加载器,来看看具体的用法吧,我们创建一个类,拥有如下内容: package com.tw.client; public class Foo { public Foo() { } public void sayHello() { System.out.println("hello world22222! (version 11)"); } } 顾名思义,此类只要调用sayHello方法,便会打印出hello world22222! (version 11)出来。 热交换处理过程如下: public static void main(String[] args) throws Exception { while (true) { run(); Thread.sleep(1000); } } /** * ClassLoader用来加载class类文件的,实现类的热替换 * 注意,需要在swap目录下,一层层建立目录com/tw/client/,然后将Foo.class放进去 * @throws Exception */ public static void run() throws Exception { CustomClassLoader customClassLoader = new CustomClassLoader("swap", new String[]{"com.tw.client.Foo"}); Class clazz = customClassLoader.loadClass("com.tw.client.Foo"); Object foo = clazz.newInstance(); Method method = foo.getClass().getMethod("sayHello", new Class[]{}); method.invoke(foo, new Object[]{}); } 当我们运行起来后,我们会将提前准备好的另一个Foo.class来替换当前这个,来看看结果吧(直接将新的Foo.class类拷贝过去覆盖即可): hello world22222! (version 11) hello world22222! (version 11) hello world22222! (version 11) hello world22222! (version 11) hello world22222! (version 11) hello world2222! (version 2) hello world2222! (version 2) hello world2222! (version 2) hello world2222! (version 2) 可以看到,当我们替换掉原来运行的类的时候,输出也就变了,变成了新类的输出结果。整体类的热交换成功。 不知道我们注意到一个细节没有,在上述代码中,我们先创建出Object的类对象,然后利用Method.invoke方法来调用类: 有人在这里会疑惑,为啥不直接转换为Foo类,然后调用类的Foo.sayHello方法呢?像下面这种方式: Foo foo2 = (Foo) clazz.newInstance(); foo2.sayHello(); 这种方式是不行的,但是大家知道为啥不行吗? 我们知道,我们写的类,一般都是被AppClassloader加载的,也就是说,你写在main启动类中的所有类,只要你写出来,那么就会被AppClassloader加载,所以,如果这里我们强转为Foo类型,那铁定是会被AppClassloader加载的,但是由于我们的clazz对象是由CustomerClassloader加载的,所以这里就会出现这样的错误: java.lang.ClassCastException: com.tw.client.Foo cannot be cast to com.tw.client.Foo 那有什么方法可以解决这个问题吗?其实是有的,就是对Foo对象抽象出一个Interface,比如说IFoo,然后转换的时候,转换成接口,就不会有这种问题了: IFoo foo2 = (IFoo) clazz.newInstance(); foo2.sayHello(); 通过接口这种方式,我们就很容易对运行中的组件进行类的热交换了,属实方便。 需要注意的是,主线程的类加载器,一般都是AppClassLoader,但是当我们创建出子线程后,其类加载器都会继承自其创建者的类加载器,但是在某些业务中,我想在子线程中使用自己的类加载器,有什么办法吗?其实这里也就是打断双亲委派机制。 由于Thread对象中已经附带了ContextClassLoader属性,所以这里我们可以很方便的进行设置和获取: //设置操作 Thread t = Thread.currentThread(); t.setContextClassLoader(loader); //获取操作 Thread t = Thread.currentThread(); ClassLoader loader = t.getContextClassLoader(); Class<?> cl = loader.loadClass(className); SPI实现类的热交换 说完基于自定义ClassLoader来进行类的热交换后,我们再来说说Java中的SPI。说到SPI相信大家都听过,因为在java中天生集成,其内部机制也是利用了自定义的类加载器,然后进行了良好的封装暴露给用户,具体的源码大家可以自定翻阅ServiceLoader类。 这里我们写个简单的例子: public interface HelloService { void sayHello(String name); } public class HelloServiceProvider implements HelloService { @Override public void sayHello(String name) { System.out.println("Hello " + name); } } public class NameServiceProvider implements HelloService{ @Override public void sayHello(String name) { System.out.println("Hi, your name is " + name); } } 然后我们基于接口的包名+类名作为路径,创建出com.tinywhale.deploy.spi.HelloService文件到resources中的META-INF.services文件夹,里面放入如下内容: 复制 com.tinywhale.deploy.spi.HelloServiceProvider com.tinywhale.deploy.spi.NameServiceProvider 然后在启动类中运行: public static void main(String...args) throws Exception { while(true) { run(); Thread.sleep(1000); } } private static void run(){ ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class); for (HelloService helloWorldService : serviceLoader) { helloWorldService.sayHello("myname"); } } 可以看到,在启动类中,我们利用ServiceLoader类来遍历META-INF.services文件夹下面的provider,然后执行,则输出结果为两个类的输出结果。之后在执行过程中,我们去target文件夹中,将com.tinywhale.deploy.spi.HelloService文件中的NameServiceProvider注释掉,然后保存,就可以看到只有一个类的输出结果了。 Hello myname Hi, your name is myname Hello myname Hi, your name is myname Hello myname Hi, your name is myname Hello myname Hello myname Hello myname Hello myname 这种基于SPI类的热交换,比自己自定义加载器更加简便,推荐使用。 自定义类加载器实现Jar部署 上面讲解的内容,一般是类的热交换,但是如果我们需要对整个jar包进行热部署,该怎么做呢?虽然现在有很成熟的技术,比如OSGI等,但是这里我将从原理层面来讲解如何对Jar包进行热部署操作。 由于内置的URLClassLoader本身可以对jar进行操作,所以我们只需要自定义一个基于URLClassLoader的类加载器即可: public class BizClassLoader extends URLClassLoader { public BizClassLoader(URL[] urls) { super(urls); } } 注意,我们打的jar包,最好打成fat jar,这样处理起来方便,不至于少打东西: <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.3</version> <configuration> <!-- 自动将所有不使用的类排除--> <minimizeJar>true</minimizeJar> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <shadedArtifactAttached>true</shadedArtifactAttached> <shadedClassifierName>biz</shadedClassifierName> </configuration> </execution> </executions> </plugin> 之后,我们就可以使用了: 复制 public static void main(String... args) throws Exception { while (true) { loadJarFile(); Thread.sleep(1000); } } /** * URLClassLoader 用来加载Jar文件, 直接放在swap目录下即可 * * 动态改变jar中类,可以实现热加载 * * @throws Exception */ public static void loadJarFile() throws Exception { File moduleFile = new File("swap\\tinywhale-client-0.0.1-SNAPSHOT-biz.jar"); URL moduleURL = moduleFile.toURI().toURL(); URL[] urls = new URL[] { moduleURL }; BizClassLoader bizClassLoader = new BizClassLoader(urls); Class clazz = bizClassLoader.loadClass("com.tw.client.Bar"); Object foo = clazz.newInstance(); Method method = foo.getClass().getMethod("sayBar", new Class[]{}); method.invoke(foo, new Object[]{}); bizClassLoader.close(); } 启动起来,看下输出,之后用一个新的jar覆盖掉,来看看结果吧: 复制 I am bar, Foo's sister, can you catch me ????????????? I am bar, Foo's sister, can you catch me ????????????? I am bar, Foo's sister, can you catch me !!!! I am bar, Foo's sister, can you catch me !!!! I am bar, Foo's sister, can you catch me !!!! I am bar, Foo's sister, can you catch me !!!! 可以看到,jar包被自动替换了。当然,如果想卸载此包,我们可以调用如下语句进行卸载: 复制 bizClassLoader.close(); 需要注意的是,jar包中不应有长时间运行的任务或者子线程等,因为调用类加载器的close方法后,会释放一些资源,但是长时间运行的任务并不会终止。所以这种情况下,如果你卸载了旧包,然后马上加载新包,且包中有长时间的任务,请确认做好业务防重,否则会引发不可知的业务问题。 由于Spring中已经有对jar包进行操作的类,我们可以配合上自己的annotation实现特定的功能,比如扩展点实现,插件实现,服务检测等等等等,用途非常广泛,大家可以自行发掘。 上面讲解的基本是原理部分,由于目前市面上有很多成熟的组件,比如OSGI等,已经实现了热部署热交换等的功能,所以很推荐大家去用一用。 说到这里,相信大家对类的热交换,jar的热部署应该有初步的概念了,但是这仅仅算是开胃小菜。由于热部署一般都是和字节码增强结合着来用的,所以这里我们先来大致熟悉一下Java Agent技术。 代码增强 技术拾忆 话说在JDK中,一直有一个比较重要的jar包,名称为rt.jar,他是java运行时环境中,最核心和最底层的类库的来源。比如java.lang.String, java.lang.Thread, java.util.ArrayList等均来源于这个类库。今天我们所要讲解的角色是rt.jar中的java.lang.instrument包,此包提供的功能,可以让我们在运行时环境中动态的修改系统中的类,而Java Agent作为其中一个重要的组件,极具特色。 现在我们有个场景,比如说,每次请求过来,我都想把jvm数据信息或者调用量上报上来,由于应用已经上线,无法更改代码了,那么有什么办法来实现吗?当然有,这也是Java Agent最擅长的场合,当然也不仅仅只有这种场合,诸如大名鼎鼎的热部署JRebel,阿里的arthas,线上诊断工具btrace,UT覆盖工具JaCoCo等,不一而足。 在使用Java Agent前,我们需要了解其两个重要的方法: 复制 /** * main方法执行之前执行,manifest需要配置属性Premain-Class,参数配置方式载入 */ public static void premain(String agentArgs, Instrumentation inst); /** * 程序启动后执行,manifest需要配置属性Agent-Class,Attach附加方式载入 */ public static void agentmain(String agentArgs, Instrumentation inst); 还有个必不可少的东西是MANIFEST.MF文件,此文件需要放置到resources/META-INF文件夹下,此文件一般包含如下内容: 复制 Premain-class : main方法执行前执行的agent类. Agent-class : 程序启动后执行的agent类. Can-Redefine-Classes : agent是否具有redifine类能力的开关,true表示可以,false表示不可以. Can-Retransform-Classes : agent是否具有retransform类能力的开关,true表示可以,false表示不可以. Can-Set-Native-Method-Prefix : agent是否具有生成本地方法前缀能力的开关,trie表示可以,false表示不可以. Boot-Class-Path : 此路径会被加入到BootstrapClassLoader的搜索路径. 在对jar进行打包的时候,最好打成fat jar,可以减少很多不必要的麻烦,maven加入如下打包内容: <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> 而MF配置文件,可以利用如下的maven内容进行自动生成: <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <archive> <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> 工欲善其事必先利其器,准备好了之后,先来手写个Java Agent尝鲜吧,模拟premain调用,main调用和agentmain调用。 首先是premain调用类 ,agentmain调用类,main调用类: //main执行前调用 public class AgentPre { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("execute premain method"); } } //main主方法入口 public class App { public static void main(String... args) throws Exception { System.out.println("execute main method "); } } //main执行后调用 public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) { System.out.println("execute agentmain method"); } } 可以看到,逻辑很简单,输出了方法执行体中打印的内容。之后编译jar包,则会生成fat jar。需要注意的是,MANIFEST.MF文件需要手动创建下,里面加入如下内容: Manifest-Version: 1.0 Premain-Class: com.tinywhale.deploy.javaAgent.AgentPre Agent-Class: com.tinywhale.deploy.javaAgent.AgentMain 由于代码是在IDEA中启动,所以想要执行premain,需要在App4a启动类上右击:Run App.main(),之后IDEA顶部会出现App的执行配置,我们需要点击Edit Configurations选项,然后在VM options中填入如下命令: -javaagent:D:\app\tinywhale\tinywhale-deploy\target\tinywhale-deploy-1.0-SNAPSHOT-biz.jar 之后启动App,就可以看到输出结果了。注意这里最好用fat jar,减少出错的机率。 复制 execute premain method execute main method 但是这里的话,我们看不到agentmain输出,是因为agentmain的运行,是需要进行attach的,这里我们对agentmain进行attach: public class App { public static void main(String... args) throws Exception { System.out.println("execute main method "); attach(); } private static void attach() { File agentFile = Paths.get("D:\\app\\tinywhale\\tinywhale-deploy\\target\\tinywhale-deploy-1.0-SNAPSHOT.jar").toFile(); try { String name = ManagementFactory.getRuntimeMXBean().getName(); String pid = name.split("@")[0]; VirtualMachine jvm = VirtualMachine.attach(pid); jvm.loadAgent(agentFile.getAbsolutePath()); } catch (Exception e) { System.out.println(e); } } } 启动app后,得到的结果为: 复制 execute premain method execute main method execute agentmain method 可以看到,整个执行都被串起来了。 讲到这里,相信大家基本上理解java agent的执行顺序和配置了吧, premain执行需要配置-javaagent启动参数,而agentmain执行需要attach vm pid。 看到这里,相信对java agent已经有个初步的认识了吧。接下来,我们就基于Java SPI + Java Agent + Javassist来实现一个插件系统,这个插件系统比较特殊的地方,就是可以增强spring框架,使其路径自动注册到component-scan路径中,颇有点霸道(鸡贼)的意思。Javassist框架的使用方式。 插件框架 玉汝于成 首先来说下这个框架的主体思路,使用Java SPI来做插件系统;使用Java Agent来使得插件可以在main主入口方法前或者是方法后执行;使用Javassist框架来进行字节码增强,即实现对spring框架的增强。 针对插件部分,我们可以定义公共的接口契约: 复制 public interface IPluginExecuteStrategy { /** * 执行方法 * @param agentArgs * @param inst */ void execute(String agentArgs, Instrumentation inst); } 然后针对premain和agentmain,利用策略模式进行组装如下: premain处理策略类 复制 public class PluginPreMainExecutor implements IPluginExecuteStrategy{ /** * 扫描加载的plugin,识别出@PreMainCondition并加载执行 */ @Override public void execute(String agentArgs, Instrumentation inst) { //获取前置执行集合 List<String> pluginNames = AgentPluginAnnotationHelper.annoProcess(PreMainCondition.class); ServiceLoader<IPluginService> pluginServiceLoader = ServiceLoader.load(IPluginService.class); //只执行带有PreMainCondition的插件 for (IPluginService pluginService : pluginServiceLoader) { if (pluginNames.contains(pluginService.getPluginName())) { pluginService.pluginLoad(agentArgs, inst); } } } } agentmain处理策略类 复制 public class PluginAgentMainExecutor implements IPluginExecuteStrategy { /** * 扫描加载的plugin,识别出@AgentMainCondition并加载执行 */ @Override public void execute(String agentArgs, Instrumentation inst) { //获取后置执行集合 List<String> pluginNames = AgentPluginAnnotationHelper.annoProcess(AgentMainCondition.class); ServiceLoader<IPluginService> pluginServiceLoader = ServiceLoader.load(IPluginService.class); for (IPluginService pluginService : pluginServiceLoader) { //只执行带有AgentMainCondition的插件 if (pluginNames.contains(pluginService.getPluginName())) { pluginService.pluginLoad(agentArgs, inst); } } } } 针对premain和agentmain,执行器工厂如下: 复制 public class AgentPluginContextFactory { /** * 创建agent pre执行上下文 * @return */ public static PluginExecutorContext makeAgentPreExecuteContext() { IPluginExecuteStrategy strategy = new PluginPreMainExecutor(); PluginExecutorContext context = new PluginExecutorContext(strategy); return context; } /** * 创建agent main执行上下文 * @return */ public static PluginExecutorContext makeAgentMainExecuteContext() { IPluginExecuteStrategy strategy = new PluginAgentMainExecutor(); PluginExecutorContext context = new PluginExecutorContext(strategy); return context; } } 编写Premain-Class和Agent-Class指定的类: 复制 public class AgentPluginPreWrapper { public static void premain(String agentArgs, Instrumentation inst) { AgentPluginContextFactory.makeAgentPreExecuteContext().execute(agentArgs, inst); } } public class AgentPluginMainWrapper { public static void agentmain(String agentArgs, Instrumentation inst) { AgentPluginContextFactory.makeAgentMainExecuteContext().execute(agentArgs, inst); } } 配置文件中指定相应的类: Manifest-Version: 1.0 Premain-Class: org.tiny.upgrade.core.AgentPluginPreWrapper Agent-Class: org.tiny.upgrade.core.AgentPluginMainWrapper Permissions: all-permissions Can-Retransform-Classes: true Can-Redefine-Classes: true 框架搭好后,来编写插件部分,插件的话,需要继承自org.tiny.upgrade.sdk.IPluginService并实现: @AgentMainCondition @Slf4j public class CodePadPluginServiceProvider implements IPluginService { @Override public String getPluginName() { return "增强插件"; } @Override public void pluginLoad(String agentArgs, Instrumentation inst) { //获取已加载的所有类 Class<?>[] classes = inst.getAllLoadedClasses(); if (classes == null || classes.length == 0) { return; } //需要将业务类进行retransform一下,这样可以避免在transform执行的时候,找不到此类的情况 for (Class<?> clazz : classes) { if (clazz.getName().contains(entity.getClassName())) { try { inst.retransformClasses(clazz); } catch (UnmodifiableClassException e) { log.error("retransform class fail:" + clazz.getName(), e); } } } //进行增强操作 inst.addTransformer(new ByteCodeBizInvoker(), true); } @Override public void pluginUnload() { } } 这里需要注意的是,在插件load的时候,我们做了class retransform操作,这样操作的原因是因为,在程序启动的时候,有时候比如一些类,会在JavaAgent之前启动,这样会造成有些类在进行增强的时候,无法处理,所以这里需要遍历并操作下,避免意外情况。 下面是具体的增强操作: @Slf4j public class ByteCodeBizInvoker implements ClassFileTransformer { /** * 在此处加载tprd-ut并利用类加载器加载 * * @param loader * @param className * @param classBeingRedefined * @param protectionDomain * @param classfileBuffer * @return * @throws IllegalClassFormatException */ @Override public byte[] transform(ClassLoader loader , String className , Class<?> classBeingRedefined , ProtectionDomain protectionDomain , byte[] classfileBuffer) throws IllegalClassFormatException { //java自带的方法不进行处理 if (loader == null) { return null; } //增强spring5的componetscan,将org.tiny路径塞入 if (className.contains("ComponentScanBeanDefinitionParser")) { try { System.out.println("增强spring"); ClassPool classPool = new ClassPool(true); classPool.appendClassPath(ByteCodeBizInvoker.class.getName()); CtClass ctClass = classPool.get(className.replace("/", ".")); ClassFile classFile = ctClass.getClassFile(); MethodInfo methodInfo = classFile.getMethod("parse"); CtMethod ctMethod = ctClass.getDeclaredMethod("parse"); addComponentScanPackage(methodInfo, ctMethod); return ctClass.toBytecode(); } catch (Exception e) { log.error("handle spring 5 ComponentScanBeanDefinitionParser error", e); } } } /** * 遍历method,直至找到ReportTracer标记类 * * @param ctMethod */ private void addComponentScanPackage(MethodInfo methodInfo, CtMethod ctMethod) throws CannotCompileException { final boolean[] success = {false}; CodeAttribute ca = methodInfo.getCodeAttribute(); CodeIterator codeIterator = ca.iterator(); //行遍历方法体 while (codeIterator.hasNext()) { ExprEditor exprEditor = new ExprEditor() { public void edit(MethodCall m) throws CannotCompileException { String methodCallName = m.getMethodName(); if (methodCallName.equals("getAttribute")) { //将org.tiny追加进去 m.replace("{ $_ = $proceed($$); $_ = $_ + \",org.tiny.upgrade\"; }"); success[0] = true; } } }; ctMethod.instrument(exprEditor); if (success[0]) { break; } } } } 从上面可以看出,我们是修改了spring中的ComponentScanBeanDefinitionParser类,并将里面的parser方法中将org.tiny.upgrade包扫描路径自动注册进去,这样当别人集成我们的框架的时候,就无须扫描到框架也能执行了。 写到这里,相信大家对整体框架有个大概的认识了。但是这个框架有个缺陷,就是我的插件jar写完后,一定要放到项目的maven dependency中,然后打包部署才行。实际上有时候,我项目上线后,根本就没有机会重新打包部署,那么接下来,我们就通过自定义Classloader来让我们的插件不仅仅可以本地集成,而且可以从网络中集成。 首先,我们需要定义自定义类加载器: public class TinyPluginClassLoader extends URLClassLoader { /** * 带参构造 * @param urls */ public TinyPluginClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } /** * 添加URL路径 * @param url */ public void addURL(URL url) { super.addURL(url); } } 这个类加载器,是不是很眼熟,和前面讲的类似,但是带了个parent classloader的标记,这是为什么呢?这个标记的意思是,当前自定义的TinyPluginClassLoader的父classloader是谁,这样的话,这个自定义类加载器就可以继承父类加载器中的信息了,避免出现问题,这个细节大家注意。 这里需要说明的是,从本地jar文件加载还是从网络jar文件加载,本质上是一样的,因为TinyPluginClassLoader是按照URL来的。 针对于本地jar文件,我们构造如下URL即可: URL url = new URL("jar:file:/D:/project/tiny-plugin-hello/target/tiny-plugin-hello-1.0-SNAPSHOT.jar!/") 针对于网络jar文件,我们构造如下URL即可: URL url = new URL("jar:http://111.111.111.111/tiny-plugin-hello-1.0-SNAPSHOT.jar!/") 这样,我们只需要定义好自定义类加载器加载逻辑即可: /** * 从jar文件中提取出对应的插件类 * * @param pluginClass * @param jarFile * @return */ public static Set<Class> loadPluginFromJarFile(Class pluginClass, JarFile jarFile, TinyPluginClassLoader tinyPluginClassLoader) { Set<Class> pluginClasses = new HashSet<Class>(); Enumeration<JarEntry> jars = jarFile.entries(); while (jars.hasMoreElements()) { JarEntry jarEntry = jars.nextElement(); String jarEntryName = jarEntry.getName(); if (jarEntryName.charAt(0) == '/') { jarEntryName = jarEntryName.substring(1); } if (jarEntry.isDirectory() || !jarEntryName.endsWith(".class")) { continue; } String className = jarEntryName.substring(0, jarEntryName.length() - 6); try { Class clazz = tinyPluginClassLoader.loadClass(className.replace("/", ".")); if (clazz != null && !clazz.isInterface() && pluginClass.isAssignableFrom(clazz)) { pluginClasses.add(clazz); } } catch (ClassNotFoundException e) { log.error("PluginUtil.loadPluginFromJarFile fail",e); } } return pluginClasses; } 之后,我们就可以用如下代码对一个具体的jar路径进行加载就行了: 复制 /** * 加载插件 * * @return */ @Override public Set<Class> loadPlugins(URL jarURL) { try { JarFile jarFile = ((JarURLConnection) jarURL.openConnection()).getJarFile(); getTinyPluginClassLoader().addURL(jarURL); return PluginUtil.loadPluginFromJarFile(IPluginService.class, jarFile, getTinyPluginClassLoader()); } catch (IOException e) { log.error("LoadPluginViaJarStrategy.loadPlugins fail", e); return null; } } 最终,我们只需要利用SPI进行动态加载: 复制 /** * 执行插件 */ public void processPlugins(URL... urls) { if (urls == null || urls.length == 0) { log.error("jar url path empty"); return; } for (URL url : urls) { pluginLoadFactory.loadJarPlugins(url); } ServiceLoader<IPluginService> serviceLoader = ServiceLoader.load(IPluginService.class, pluginLoadFactory.getPluginLoader()); for (IPluginService pluginService : serviceLoader) { pluginService.Process(); } } 这样,我们不仅实现了插件化,而且我们的插件还支持从本地jar文件或者网络jar文件加载。由于我们利用了agentmain对代码进行增强,所以当系统检测到我这个jar的时候,下一次执行会重新对代码进行增强并生效。 总结 到这里,我们的用餐进入到尾声了。也不知道这餐,您享用的是否高兴? 其实本文的技术,从双亲委派模型到自定义类加载器,再到基于自定义类加载器实现的类交换,基于Java SPI实现的类交换,最后到基于Java SPI+ Java Agent + Javassist实现的插件框架及框架支持远程插件化,来一步一步的向读者展示所涉及的知识点。当然,由于笔者知识有限,疏漏之处,还望海涵,真诚期待我的抛砖,能够引出您的玉石之言。

优秀的个人博客,低调大师

声明式 HTTP 框架 Forest v1.5.21 发布!

Forest 介绍 Forest 是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求 在 Gitee 上现已超过 2.4k star Forest 如何使用 Forest 不需要您编写具体的 HTTP 调用过程,只需要您定义一个接口,然后通过 Forest 注解将 HTTP 请求的信息添加到接口的方法上即可。请求发送方通过调用您定义的接口便能自动发送请求和接受请求的响应 Forest 的工作原理 Forest 会将您定义好的接口通过动态代理的方式生成一个具体的实现类,然后组织、验证 HTTP 请求信息,绑定动态数据,转换数据形式,SSL 验证签名,调用后端 HTTP API (httpclient 等 API) 执行实际请求,等待响应,失败重试,转换响应数据到 Java 类型等脏活累活都由这动态代理的实现类给包了。 请求发送方调用这个接口时,实际上就是在调用这个干脏活累活的实现类 重点更新内容 此次版本发布主要解决了URL在某些情况下的解析错误问题,以及对字符串模板进行手动URLEncode的支持 任何包含特殊字符的URL地址或Body中的表单信息要发送请求,都要进过一定程度的UREncode以符合HTTP相关标准,才能正常发出请求 而HTTP请求中的不同部分对URLEncode强度要求是不同的,比如Body表单格式字符串的要求相对宽松,可以包含{,},空格等字符串,而Query参数值部分则必须将其转义才能正常发送 所以Forest在默认情况下为HTTP请求已经URL中的不同部分提供了各自合适的编码强度(最大限度宽松)的 URLEncode 但有些情况默认的URLEncode也无法满足要求: 不同服务端接口对编码的强度要求可能是不同的,比如要求Query参数必须是最严格的编码 已经如果在URL中大范围使用${变量名}这样变量引用的时候,Forest也无法准备判断您引入的变量是URL中具体那个部分,从而也无法给出准确的编码强度 输入一个有歧义的字符串到字符串模板中 所以此时需要进行手动URLEncode,为此提供了在字符串模板中可供调用的内置函数: encode(str): 严格编码,最大的编码强度 encodeQuery(str): Query参数编码,对URL中的参数值部分字符串进行编码,强度适中以最大限度适应HTTP规范 encodePath(str): Path路径编码,对URL中域名+后端+/后的路径部分字符串进行编码 encodeForm(str): Form表单编码,对Body中x-www-form-urlencoded格式的请求体字符串进行编码,强度最宽松 encodeUserInfo(str): UserInfo编码,对URL中用户名密码验证信息部分字符串进行编码 例子: // 在{}中调用encode函数对userInfo变量进行严格URLEncode编码 @Get("http://{encode(userInfo)}@localhost:8080/hello/user") ForestResponse<String> doGet(@Var("userInfo") String userInfo); 若调用该接口参数如下: client.doGet("xxx/yyy/foo"); 则产生的请求为: GET http://xxx%2Fyyy%2Ffoo@localhost:8080/hello/user HTTP 如若不用encode函数的话,则请求会变成 GET http://xxx/yyy/foo@localhost:8080/hello/user HTTP 此时xxx/yyy/xxx中的/会使地址产生歧义,将xxx错误地解析为域名,显然这样的地址是无法正常发送出去的 文档和示例 项目官网 JavaDoc Demo 工程 本次更新 新增特性: feat: 字符串模板支持手动URLEncode (#I58D1C:字符串模板支持手动URLEncode) 修复的问题: fix: url在某些场景下的解析错误 (#I56XDM:url在某些场景下的解析错误)

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册