首页> 标签> Spring
"Spring"
共 19566 条结果
全部 问答 文章 公开课 课程 电子书 技术圈 体验
开发环境中使用Maven|学习笔记
开发者学堂课程【项目管理工具Maven学习:开发环境中使用Maven】学习笔记,与课程紧密联系,让用户快速学习知识。课程地址:https://developer.aliyun.com/learning/course/23目录:一、仓库二、Eclipse 一、仓库1、坐标关于坐标,首先会想到平面几何,x,y!!任何一个坐标都能够唯一标识该平面中的一点。在实际生活中,将地址看成是一个坐标。省,市,县,街道,门牌号等等。Maven的世界中拥有数量巨大的构件,也就是平时用的一些jar,war等文件。如果没有Maven我们只能去各个官方网站查找下载,去本地搜索,论坛上发帖询问。想要找一个jar包,大量的时间花费在搜索,浏览网页等工作上面。因为没有统一的规范,所以无法自动化处理。现在,Maven定义了这样一组规则:世界上任何一个构件都可以使用Maven坐标唯一标识,Maven坐标元素包括: groupld 、artifactld、version、packaing.classifier.Maven提供了一个中央仓库。该中央仓库包含了世界上大部分流行的开源项目构件。在开发自己的项目的时候,也需要为其定义适当的坐标,这是Maven强制要求的,在这个基础上,其他Maven项目才能引用该项目生成的构件。2、坐标详解Maven坐标为各种组件引入了秩序,任何一个组件都必须明确定义自己的坐标。Groupld:定义当前Maven项目隶属的实际项目。由于Maven中模块的概念,因此一个实际项目往往会被划分为很多模块。比如spring是一个实际项目,其对应的Maven 模块会有很多,如spring-core,spring-webmvc 等。groupId:的表示方法与java包名的表示方法类似,通常与域名反向一一对应。ArtifactId:该元素定义实际项目中的一个Maven模块,推荐的做法是使用实际项目名称作为atifactId的前缀。比如: spring-bean。Version:该元素定义Maven项目当前所处的版本。Packaging:该元素定义Maven项目的打包方式。首先,打包方式通常与所生成构件的文件扩展名对应。Maven默认的打包方式是jat。也可以是war,ear,可执行的jar。Classifier:该元素用来帮助定义构建输出的一些附属构件。比如javadoc和sourcesjar。这是java文档和源代码。3、仓库的概念●当第一次运行Maven命令的时候,你需要Intenet链接,因为它需要从网上下载一些文件。那么它从哪里下载呢?它是从Maven默认的远程库下载的。 这个远程仓库由Maven 的核心插件和可供下载的jar文件。●对于Maven来说,仓库只分为两类:本地仓库和远程仓库。当Maven根据坐标寻找构件的时候,它首先会查看本地仓库,如果本地仓库存在,则直接使用;如果本地没有,Maven就会去远程仓库查找,发现需要的构件之后,下载到本地仓库再使用。如果本地仓库和远程仓库都没有,Maven就会报错。●提示:远程仓库分为三种:中央仓库,私服,其他公共库。 中央仓库是默认配置下,Maven下载jar包的地方。 ◆私服是另一种特殊的远程仓库,为了节省带宽和时间,应该在局域网内架设一个私有的仓库服务器,用其代理所有外部的远程仓库。内部的项目还能部署到私服上供其他项目使用。●私服可以通过nexus来搭建Maven。●为什么要搭建nexus私服,原因很简单,有些公司都不提供外网给项目组人员,因此就不能使用maven访问远程的仓库地址,所以很有必要在局域网里找一台有外网权限的机器,搭建nexus私服,然后开发人员连到这台私服上,这样的话就可以通过这台搭建了nexus私服的电脑访问maven的远程仓库。①首先确定环境安装好maven,jdk等必负的环境②这些都准备好之后,去下载最新版本的nexus下载地址:http://www.sonatype.org/nexus/go本地安装的是nexus-22-01-bundle,最新的版本是nexus-2.4.0-09-bundle③打开目录nexus-2.4.0-09-bundlenexus-24.0-09\binsw这个目录下面你会发现有很多系统版本的nexus环增。二、EclipseEclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。幸运的是,Eclipse 附带了一个标准的插件集,包括Java开发工具(Java Development Kit,JDK)。虽然大多数用户很乐于将Eclipse 当作Java 集成开发环境(IDE)来使用,但Eclipse 的目标却不仅限于此。Eclipse 还包括插件开发环境(Plug-in Development Environment,PDE),这个组件主要针对希望扩展Eclipse 的软件开发人员,因为它允许他们构建与Eclipse 环境无缝集成的工具。由于Eclipse 中的每样东西都是插件,对于给Eclipse 提供插件,以及给用户提供一致和统一的集成开发环境而言,所有工具开发人员都具有同等的发挥场所。这种平等和一致性并不仅限于 Java 开发工具。尽管Eclipse 是使用Java语言开发的,但它的用途并不限于Java 语言;例如,支持诸如C/C++、COBOL、PHP等编程语言的插件已经可用,或预计将会推出。Eclipse 框架还可用来作为与软件开发无关的其他应用程序类型的基础,比如内容管理系统。基于Eclipse 的应用程序的一个突出例子是IBM Rational Software Architect,它构成了 IBMJava 开发工具系列的基础。
文章
IDE  ·  Java  ·  Go  ·  PHP  ·  Maven  ·  开发工具  ·  Android开发  ·  C++  ·  开发者  ·  Spring
2022-06-29
《我想进大厂》之Spring夺命连环10问
1.说说Spring 里用到了哪些设计模式?单例模式:Spring 中的 Bean 默认情况下都是单例的。无需多说。工厂模式:工厂模式主要是通过 BeanFactory 和 ApplicationContext 来生产 Bean 对象。代理模式:最常见的 AOP 的实现方式就是通过代理来实现,Spring主要是使用 JDK 动态代理和 CGLIB 代理。模板方法模式:主要是一些对数据库操作的类用到,比如 JdbcTemplate、JpaTemplate,因为查询数据库的建立连接、执行查询、关闭连接几个过程,非常适用于模板方法。2.谈谈你对IOC 和 AOP 的理解?他们的实现原理是什么?IOC 叫做控制反转,指的是通过Spring来管理对象的创建、配置和生命周期,这样相当于把控制权交给了Spring,不需要人工来管理对象之间复杂的依赖关系,这样做的好处就是解耦。在Spring里面,主要提供了 BeanFactory 和 ApplicationContext 两种 IOC 容器,通过他们来实现对 Bean 的管理。AOP 叫做面向切面编程,他是一个编程范式,目的就是提高代码的模块性。Srping AOP 基于动态代理的方式实现,如果是实现了接口的话就会使用 JDK 动态代理,反之则使用 CGLIB 代理,Spring中 AOP 的应用主要体现在 事务、日志、异常处理等方面,通过在代码的前后做一些增强处理,可以实现对业务逻辑的隔离,提高代码的模块化能力,同时也是解耦。Spring主要提供了 Aspect 切面、JoinPoint 连接点、PointCut 切入点、Advice 增强等实现方式。3. JDK 动态代理和 CGLIB 代理有什么区别?JDK 动态代理主要是针对类实现了某个接口,AOP 则会使用 JDK 动态代理。他基于反射的机制实现,生成一个实现同样接口的一个代理类,然后通过重写方法的方式,实现对代码的增强。而如果某个类没有实现接口,AOP 则会使用 CGLIB 代理。他的底层原理是基于 asm 第三方框架,通过修改字节码生成成成一个子类,然后重写父类的方法,实现对代码的增强。4. Spring AOP 和 AspectJ AOP 有什么区别?Spring AOP 基于动态代理实现,属于运行时增强。AspectJ 则属于编译时增强,主要有3种方式:编译时织入:指的是增强的代码和源代码我们都有,直接使用 AspectJ 编译器编译就行了,编译之后生成一个新的类,他也会作为一个正常的 Java 类装载到JVM。编译后织入:指的是代码已经被编译成 class 文件或者已经打成 jar 包,这时候要增强的话,就是编译后织入,比如你依赖了第三方的类库,又想对他增强的话,就可以通过这种方式。加载时织入:指的是在 JVM 加载类的时候进行织入。总结下来的话,就是 Spring AOP 只能在运行时织入,不需要单独编译,性能相比 AspectJ 编译织入的方式慢,而 AspectJ 只支持编译前后和类加载时织入,性能更好,功能更加强大。5. FactoryBean 和 BeanFactory有什么区别?BeanFactory 是 Bean 的工厂, ApplicationContext 的父类,IOC 容器的核心,负责生产和管理 Bean 对象。FactoryBean 是 Bean,可以通过实现 FactoryBean 接口定制实例化 Bean 的逻辑,通过代理一个Bean对象,对方法前后做一些操作。6.SpringBean的生命周期说说?SpringBean 生命周期简单概括为4个阶段:实例化,创建一个Bean对象填充属性,为属性赋值初始化如果实现了xxxAware接口,通过不同类型的Aware接口拿到Spring容器的资源如果实现了BeanPostProcessor接口,则会回调该接口的postProcessBeforeInitialzation和postProcessAfterInitialization方法如果配置了init-method方法,则会执行init-method配置的方法销毁容器关闭后,如果Bean实现了DisposableBean接口,则会回调该接口的destroy方法如果配置了destroy-method方法,则会执行destroy-method配置的方法7.Spring是怎么解决循环依赖的?首先,Spring 解决循环依赖有两个前提条件:不全是构造器方式的循环依赖必须是单例基于上面的问题,我们知道Bean的生命周期,本质上解决循环依赖的问题就是三级缓存,通过三级缓存提前拿到未初始化的对象。第一级缓存:用来保存实例化、初始化都完成的对象第二级缓存:用来保存实例化完成,但是未初始化完成的对象第三级缓存:用来保存一个对象工厂,提供一个匿名内部类,用于创建二级缓存中的对象假设一个简单的循环依赖场景,A、B互相依赖。A对象的创建过程:创建对象A,实例化的时候把A对象工厂放入三级缓存A注入属性时,发现依赖B,转而去实例化B同样创建对象B,注入属性时发现依赖A,一次从一级到三级缓存查询A,从三级缓存通过对象工厂拿到A,把A放入二级缓存,同时删除三级缓存中的A,此时,B已经实例化并且初始化完成,把B放入一级缓存。着继续创建A,顺利从一级缓存拿到实例化且初始化完成的B对象,A对象创建也完成,删除二级缓存中的A,同时把A放入一级缓存最后,一级缓存中保存着实例化、初始化都完成的A、B对象因此,由于把实例化和初始化的流程分开了,所以如果都是用构造器的话,就没法分离这个操作,所以都是构造器的话就无法解决循环依赖的问题了。8. 为什么要三级缓存?二级不行吗?不可以,主要是为了生成代理对象。因为三级缓存中放的是生成具体对象的匿名内部类,他可以生成代理对象,也可以是普通的实例对象。使用三级缓存主要是为了保证不管什么时候使用的都是一个对象。假设只有二级缓存的情况,往二级缓存中放的显示一个普通的Bean对象,BeanPostProcessor去生成代理对象之后,覆盖掉二级缓存中的普通Bean对象,那么多线程环境下可能取到的对象就不一致了。9.Spring事务传播机制有哪些?PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,这也是通常我们的默认选择。PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。‘10.最后,说说Spring Boot 启动流程吧?这个流程,网上一搜基本都是这张图了,我也不想再画一遍了。那其实主要的流程就几个步骤:准备环境,根据不同的环境创建不同的Environment准备、加载上下文,为不同的环境选择不同的Spring Context,然后加载资源,配置Bean初始化,这个阶段刷新Spring Context,启动应用最后结束流程
文章
设计模式  ·  缓存  ·  Java  ·  编译器  ·  数据库  ·  Spring  ·  容器
2022-06-28
微信小程序实现蓝牙开门前后端项目(一)
物料准备一个蓝牙模块详细设计业务流程图后台管理功能1、管理员(超级管理员)可以登录后台,维护小区的基本信息、维护小区物业的账号信息、查询操作日志、查询用户列表2、小区物业可以登录后台,维护小区业主的账号信息,查询用户列表小程序开门功能1、用户(业主)打开微信小程序,小程序检测用户是否登录状态,未登录,跳转到登录页,提示登录;已登录,跳转到开门页面2、检测用户是否开启蓝牙,未开启蓝牙,提示:请开启蓝牙;已开启蓝牙,检测附近是否有蓝牙设备,没有蓝牙设备,提示:没有检测到蓝牙设备,请联系物业或密码刷卡开门(因为开不了门,需要咨询物业);有蓝牙设备,使用预先下载的秘钥连接蓝牙设备,如果连接失败,重试3次,超过3次未成功,提示:设备连接失败,请联系物业或密码刷卡开门;如果连接成功,开门按钮高亮显示,用户可以点击开门按钮;3、用户点击开门按钮后,小程序会通过蓝牙接口调用蓝牙设备,发送开门指令;如果开门失败,会快速重试3次,超过3次,提示:设备连接失败,请联系物业或密码刷卡开门;如果开门成功,会有手机震动提醒。4、用户也可以在开门界面,摇动手机开门,流程同3小程序界面设计一共就两个页面,“我的页面”和“首页”。“我的页面” - 由于是第一版,主要是实现开门功能,我的页面非常简单,只有一个退出登录按钮,后期会增加访客邀请,开门记录等功能“首页” - 开门页面是该小程序的主页页面,由蓝牙状态、开门状态和开门按钮组成,开门按钮设计的比较大并且醒目,方便用户点击管理后台设计管理后台因为是管理人员使用,页面要求不是很高,因此使用了开源前端框架自带主题,该前端框架AdminLTE一共有8种主题可切换技术选型由于本软件跟后端没有太多交互,只是对用户进行基本的账户认证,对后端性能要求较低,因此后端只需采用SpringBoot单体应用即可。数据库采用流行的Mysql其他技术栈:Spring、Freemarker、Shiro、Mybatis前端:微信小程序表结构设计功能接口小程序调用接口小程序调用后端接口比较简单,主要就是验证账户,和保存用户信息用户登录接口地址:/api/mini/user/userLogin请求方式:POST请求字段:名称说明username用户名password密码返回字段:名称说明id用户idusername用户名小程序登录获取code接口地址:/api/mini/user/getLogin请求方式:POST请求字段:名称说明code小程序codeminiUserVO小程序用户信息Object+avatarUrl头像+province省+其他略 返回字段:名称说明sessionId状态idopenid小程序用户唯一idunionid userInfo小程序用户信息Object+avatarUrl头像+province省+其他略 好了,夜已深,今天就到此为止,本文专门用来说明该软件的业务流程、功能设计、接口设计和表结构设计,这是非常有必要的,这将帮我们理清开发思路,为下文的编码实现打下坚实的基础。实际上我们做任何项目都需要先设计(总体设计->详细设计->评审),评审通过后才开始编码。
文章
小程序  ·  前端开发  ·  Java  ·  关系型数据库  ·  MySQL  ·  数据库连接  ·  API  ·  数据安全/隐私保护  ·  Spring  ·  mybatis
2022-06-28
学习资源
搞学习TED(最优质的演讲):https://www.ted.com/谷粉学术:https://gfsoso.99lb.net/scholar.html大学资源网:http://www.dxzy163.com/简答题:http://www.jiandati.com/网易公开课:https://open.163.com/ted/网易云课堂:https://study.163.com/中国大学MOOC:www.icourse163.org哔哩哔哩弹幕网:www.bilibili.com我要自学网:www.51zxw.net知乎:www.zhihu.com学堂在线:www.xuetangx.com爱课程:www.icourses.cn猫咪论文:https://lunwen.im/iData(论文搜索):www.cn-ki.net文泉考试:https://www.wqkaoshi.comCSDN:https://www.csdn.net/基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。项目地址:https://github.com/YunaiV/ruoyi-vue-pro找书籍书栈网(极力推荐):https://www.bookstack.cn/码农之家(计算机电子书下载):www.xz577.com鸠摩搜书:www.jiumodiary.com云海电子图书馆:www.pdfbook.cn周读(书籍搜索):ireadweek.com知轩藏书:http://www.zxcs.me/脚本之家电子书下载:https://www.jb51.net/books/搜书VIP-电子书搜索:http://www.soshuvip.com/all.html书格(在线古籍图书馆):https://new.shuge.org/caj云阅读:http://cajviewer.cnki.net/cajcloud/必看网(人生必看的书籍):https://www.biikan.com/基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。项目地址:https://github.com/YunaiV/onemall冷知识 / 黑科技上班摸鱼必备(假装电脑系统升级):http://fakeupdate.net/PIECES 拼图(30 个 CSS 碎片进行拼图,呈现 30 种濒临灭绝的动物):http://www.species-in-pieces.com/图片立体像素画:https://pissang.github.io/voxelize-image/福利单词(一个不太正经的背单词网站):http://dict.ftqq.com查无此人(刷新网站,展现一张AI 生成的人脸照片):https://thispersondoesnotexist.com/在线制作地图图例:https://mapchart.net/创意光线绘画:http://weavesilk.com/星系观察:https://stellarium-web.org/煎蛋:http://jandan.net/渣男-说话的艺术:https://lovelive.tools/全历史:https://www.allhistory.com/iData:https://www.cn-ki.net/术语在线:http://www.termonline.cn/写代码GitHub:https://github.com/码云:https://gitee.com/源码之家:https://www.mycodes.net/JSON to Dart:https://javiercbk.github.io/json_to_dart/Json在线解析验证:https://www.json.cn/在线接口测试(Getman):https://getman.cn/资源搜索DogeDoge搜索引擎:www.dogedoge.com秘迹搜索:https://mijisou.com/小白盘:https://www.xiaobaipan.com/云盘精灵(资源搜索):www.yunpanjingling.com虫部落(资源搜索):www.chongbuluo.com如风搜(资源搜索):http://www.rufengso.net/爱扒:https://www.zyboe.com/小工具奶牛快传(在线传输文件利器):cowtransfer.com文叔叔(大文件传输,不限速):https://www.wenshushu.cn/云端超级应用空间(PS,PPT,Excel,Ai):https://uzer.me/香当网(年终总结,个人简历,事迹材料,租赁合同,演讲稿):https://www.xiangdang.net/二维码生成:https://cli.im/搜狗翻译:fanyi.sogou.com熵数(图表制作,数据可视化):https://dydata.io/appv2/#/pages/index/home拷贝兔:https://cp.anyknew.com/图片无限变放大:http://bigjpg.com/zh幕布(在线大纲笔记工具):mubu.com在线转换器(在线转换器转换任何测量单位):https://zh.justcnw.com/调查问卷制作:https://www.wenjuan.com/果核剥壳(软件下载):https://www.ghpym.com/软件下载:https://www.unyoo.com/MSDN我告诉你(windows10系统镜像下载):https://msdn.itellyou.cn/导航页(工具集)世界各国网址大全:http://www.world68.com/小森林导航:http://www.xsldh6.com/简捷工具:http://www.shulijp.com/NiceTool.net 好工具网:http://www.nicetool.net/现实君工具箱(综合型在线工具集成网站):http://tool.uixsj.cn/蓝调网站:http://lcoc.top/偷渡鱼:https://touduyu.com/牛导航:http://www.ziliao6.com/小呆导航:https://www.webjike.com/index.html简法主页:http://www.jianfast.com/KIM主页:https://kim.plopco.com/聚BT:https://jubt.net/cn/index.html精准云工具合集:https://jingzhunyun.com/兔2工具合集:https://www.tool2.cn/爱资料工具(在线实用工具集合):www.toolnb.com工具导航:https://hao.logosc.cn/看视频阿木影视:https://www.aosk.online/电影推荐(分类别致):http://www.mvcat.comAPP影院:https://app.movie去看TV:https://www.qukantv.net/动漫视频网:http://www.zzzfun.com/94神马电影网:http://www.9rmb.com/NO视频官网:http://www.novipnoad.com/蓝光画质电影:http://www.languang.co/在线看剧:http://dy.27234.cn/大数据导航:http://hao.199it.com/多功能图片网站:https://www.logosc.cn/so/牛牛TV:http://www.ziliao6.com/tv/VideoFk解析视频:http://www.videofk.com/蓝调网站:http://lcoc.top/vip2.3/永久资源采集网:http://www.yongjiuzy1.com/学设计码力全开(产品/设计师/独立开发者的资源库):https://www.maliquankai.com/designnav/免费音频素材:https://icons8.cn/music新CG儿(视频素材模板,无水印+免费下载):https://www.newcger.com/Iconfont(阿里巴巴矢量图标库):https://www.iconfont.cn/小图标下载:https://www.easyicon.net/Flight Icon:https://www.flighticon.co/第一字体转换器:http://www.diyiziti.com/doyoudosh(平面设计):www.doyoudo.com企业宣传视频在线制作:https://duomu.tv/MAKE海报设计官网:http://maka.im/一键海报神器:https://www.logosc.cn/photo/utm_source=hao.logosc.cn&utm_medium=referral字由(字体设计):http://www.hellofont.cn/查字体网站:https://fonts.safe.360.cn/爱给网(免费素材下载的网站,包括音效、配乐,3D、视频、游戏,平面、教程):http://www.aigei.com/在线视频剪辑:https://bilibili.clipchamp.com/editor搞文档即书(在线制作PPT):https://www.keysuper.com/PDF处理:https://smallpdf.com/cnPDF处理:https://www.ilovepdf.com/zh-cnPDF处理:https://www.pdfpai.com/PDF处理:https://www.hipdf.cn/图片压缩,PDF处理:https://docsmall.com/腾讯文档(在线协作编辑和管理文档):docs.qq.comProcessOn(在线协作制作结构图):www.processon.comiLovePDF(在线转换PDF利器):www.ilovepdf.comPPT在线制作:https://www.woodo.cn/PDF24工具(pdf处理工具):https://tools.pdf24.org/enIMGBOT(在线图片处理):www.imgbot.ai福昕云编辑(在线编辑PDF):edit.foxitcloud.cnTinyPNG(在线压缩图片):tinypng.comUZER.ME(在线使用各种大应用,在线使用CAD,MATLAB,Office三件套):uzer.me优品PPT(模板下载):http://www.ypppt.com/第一PPT(模板下载):http://www.1ppt.com/xiazai/三顿PPT导航:sandunppt.comExcel函数表:https://support.office.com/zh-cn/article/excel-%E5%87%BD%E6%95%B0%EF%BC%88%E6%8C%89%E5%AD%97%E6%AF%8D%E9%A1%BA%E5%BA%8F%EF%BC%89-b3944572-255d-4efb-bb96-c6d90033e188找图片电脑壁纸:http://lcoc.top/bizhi/https://unsplash.com/https://pixabay.com/https://www.pexels.com/https://visualhunt.com/https://www.ssyer.com/彼岸图网:http://pic.netbian.com/极像素(超高清大图):https://www.sigoo.com/免费版权图片搜索:https://www.logosc.cn/so/
文章
人工智能  ·  JSON  ·  前端开发  ·  JavaScript  ·  Java  ·  数据库连接  ·  定位技术  ·  数据格式  ·  Spring  ·  mybatis
2022-06-28
设计模式:单例、原型和生成器
>在这篇文章中,我们将重点介绍其余的创建模式:Singleton,Builder和Prototype。在我看来,这些模式不如工厂重要。然而,了解它们仍然很有用。我将提供UML描述,简单的java示例(这样即使你不了解java,你也可以理解),并提出来自着名Java框架或API的真实示例。@[toc]# 创造模式创建模式是处理对象初始化并克服构造函数限制的设计模式。四**人帮**在他们的书“【设计模式:可重用面向对象软件的元素】中描述了其中的五个:- 单身人士,- 建筑工人,- 原型,- 抽象工厂,- 工厂模式。![在这里插入图片描述](https://img-blog.csdnimg.cn/afd3790f031347bdadb72ad2ed2ccb9c.png)自本书出版(1994年)以来,已经发明了许多创造模式:- 其他类型的工厂(如静态工厂),- 池模式,- 惰性初始化,- 依赖注入,- 服务定位器,- ...>在这篇文章中,我们只关注我还没有描述过的GoF的创造模式的其余部分。正如我在引言中所说,它们不如工厂重要,因为你可以没有它们(而工厂是许多应用程序和框架的支柱)。但是,它们很有用,与工厂不同,它们不会使代码更难阅读。# 单例模式这种模式是最著名的。在过去的几十年里,它被过度使用,但自那以后它的受欢迎程度有所下降。我个人避免使用它,因为它使代码**更难以进行单元测试**并创建**紧密耦合**。我更喜欢使用处理类的授权实例数的工厂(如Spring容器),我们将讨论这种方法。我认为你应该避免单例模式。事实上,这种模式最重要的用途是能够在面试官问“什么是单例?”时回答他。这种模式非常有争议,仍然有人赞成它。话虽如此,根据GoF的说法,单例旨在:> *“确保一个类只有一个实例,并提供对它的全局访问点”*因此,类是单例有 2 个要求:- 拥有唯一实例- 可从任何地方访问有些人只考虑第一个要求(比如几年前的我)。在这种情况下,该类只是一个**实例**。让我们看一下如何在UML中执行单例[![单例模式](https://img-blog.csdnimg.cn/img_convert/499cb7a47089bbfc2d96c5cafb6faa65.png)](http://coding-geek.com/wp-content/uploads/2015/06/singleton.png)在此 UML 图中,单例类有 3 项:- 类属性(实例):此属性包含单例类的唯一实例。- 一个类公共方法(getInstance()):它提供了获取类Singleton的唯一实例的唯一方法。可以从任何地方调用该方法,因为它是类方法(而不是实例方法)。- 私有构造函数(Singleton()):它阻止任何人使用构造函数实例化单例。在此示例中,需要 Singleton 实例的开发人员将调用 Singleton.getInstance() 类方法。单例类中的单例实例可以是:- 预初始化(这意味着在有人调用 getInstance()之前,它被实例化了)- lazy-initialized(这意味着它是在第一次调用getInstance()期间实例化的)当然,真正的单例还有其他方法和属性来执行其业务逻辑。## Java 实现这是使用预实例化方法在Java中创建单例的非常简单的方法。```javapublic class SimpleSingleton {    private static final SimpleSingleton INSTANCE = new SimpleSingleton();    private SimpleSingleton() { }    public static SimpleSingleton getInstance(){        return INSTANCE;    }}```使用这种方式,当类装入器装入类时,仅创建一次单一实例。如果代码中从未使用过该类,则不会实例化该实例(因为 JVM 的类装入器不会加载它),因此会浪费内存。但是,如果该类出现并且您不使用它(例如,如果它仅在非常非常罕见的条件下使用),则单例将无条件初始化。除非您的单例占用大量内存,否则应以这种方式使用。不过,如果你只需要在真正使用时创建单例(惰性初始化),这里有一种方法可以在多线程环境中执行此操作。这部分有点棘手,因为它涉及线程一致性。```javapublic class TouchySingleton {    private static volatile TouchySingleton instance;    private TouchySingleton() {    }    public static TouchySingleton getInstance() {        if (instance == null) {            synchronized (TouchySingleton.class) {                if (instance == null) {                    instance = new TouchySingleton();                }            }        }        return instance;    }}```正如我所说,它真的更难阅读(这就是为什么预先实例化的方式更好)。此单例涉及一个锁,以避免 2 个线程同时调用 getInstance() 创建 2 个实例。由于锁的成本很高,因此首先要进行没有锁的测试,然后使用锁进行测试(这是双重检查的锁定),以便在实例已经存在时不使用锁。另一个特殊性是,实例必须是易失性的,以确保在创建实例时,其状态在不同的处理器内核上是相同的。## 何时需要使用单例?- 当您只需要一个资源(数据库连接,套接字连接...- 避免无状态类的多个实例以避免内存浪费- 出于业务原因**您不应该使用单例在不同对象之间共享变量/数据,因为它会产生非常紧密的耦合!**## 为什么不使用单例?一开始,我说你不应该使用单例,因为你获得单例的方式。它基于一个类函数,可以在代码中的任何位置调用该函数。我在[stackoverflow上](https://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons)读到了一个很好的答案,给出了它不好的4个原因:- 使用单例,可以隐藏类之间的依赖关系,而不是通过接口公开它们。这意味着您需要阅读每个方法的代码,以了解一个类是否正在使用另一个类。- 它们违反了[单一责任原则](https://en.wikipedia.org/wiki/Single_responsibility_principle):它们控制自己的创建和生命周期(使用惰性初始化,单例在创建时选择)。一个类应该只关注它要做什么。如果你有一个管理人员的单例,它应该只管理人员,而不是如何/何时创建。- 它们本质上会导致代码紧密耦合。这使得伪造或嘲笑它们进行单元测试变得非常困难。- 它们在应用程序的生存期内携带状态(对于有状态单例)。  - 它使单元测试变得困难,因为您可能最终会遇到需要订购测试的情况,这是一种无稽之谈。根据定义,每个单元测试都应该彼此独立。  - 此外,它使代码的可预测性降低。好吧,所以单例很糟糕。但是你应该使用什么呢?## 使用单个实例而不是单一实例单例只是一种特定类型的单个实例,可以使用其类方法到达任何地方。如果删除此第二个要求,则会删除许多问题。但是,如何处理单个实例呢?一种可能的方法是使用工厂和依赖注入来管理单个实例(这将是未来文章的主题)。让我们举个例子来理解:- 您有一个需要唯一数据库连接实例的 PersonBusiness 类。- PersonBusiness 将具有 DatabaseConnection 属性,而不是使用单例来获取此连接。- 此属性将由其构造函数在 PersonBusiness 的实例化时注入。当然,您可以注入任何类型的数据库连接:  - 适用于您的开发环境的 MysqlDatabaseConnection  - 面向生产环境的 OracleDatabaseConnection  - 用于单元测试的模拟数据库连接- 在此阶段,没有任何操作可以阻止数据库连接是唯一的。这就是工厂有用的地方。您将PersonBusiness的创建委托给工厂,该工厂还负责数据库连接的创建:  - 它选择要创建的连接类型(例如,使用指定连接类型的属性文件)  - 它确保数据库连接是唯一的。如果你不明白我刚才说了什么,看看下一个java示例,然后再次重读这部分,它应该更全面。否则,请随时告诉我。下面是一个Java中的例子,其中工厂创建了一个MysqlDatabaseConnection,但你可以想象一个更复杂的工厂,它根据属性文件或环境变量决定连接的类型。```java////////////////An interface that represents a database connectionpublic interface DatabaseConnection {    public void executeQuerry(String sql);}////////////////A concrete implementation of this interface////////////////In this example it's for a mysql database connectionpublic class MysqlDatabaseConnection implements DatabaseConnection {    public MysqlDatabaseConnection() {        // some stuff to create the database connection    }    public void executeQuerry(String sql) {        // some stuff to execute a SQL query        // on the database    }}////////////////Our business class that needs a connectionpublic class PersonBusiness {    DatabaseConnection connection;    //dependency injection using the constructor    // it is a singleton because the factory that    //creates a PersonBusiness object ensure that    //UniqueDatabaseConnection has only one instance    PersonBusiness(DatabaseConnection connection){        this.connection = connection;    }        //a method that uses the injected singleton    public void deletePerson(int id){        connection.executeQuerry("delete person where id="+id);    }}////////////////A factory that creates business classes//////////////// with a unique MysqlDatabaseConnectionpublic class Factory {    private static MysqlDatabaseConnection databaseConnection = new MysqlDatabaseConnection();    public static MysqlDatabaseConnection getUniqueMysqlDatabaseConnection(){        return databaseConnection;    }    public static PersonBusiness createPersonBusiness(){        //we inject a MysqlDataConnection but we could chose        //another connection that implements the DatabaseConnection        //this is why this is a loose coupling        return new PersonBusiness(databaseConnection);    }}```这不是一个很好的例子,因为PersonBusiness可以有一个实例,因为它没有状态。但你可以想象,有一个ContractBusiness和一个HouseBusiness也需要那个独特的DatabaseConnection。不过,我希望你看到,使用**依赖注入+ 工厂**,你最终会在你的业务类中得到一个数据库连接的单个实例,就像你使用了一个单例一样。但这一次,它是一个松散的耦合,这意味着你可以很容易地使用MockDatabaseConnection来测试PersonBusiness类,而不是使用MysqlDatabaseConnection。此外,很容易知道PersonBusiness正在使用DatabaseConnection。你只需要查看类的属性,而不是类的2000行代码中的一行(好吧,想象一下这个类有很多函数,整体需要2000行代码)。这种方法被大多数Java框架(Spring,Hibernate...)和Java容器(EJB容器)使用。它不是真正的单例,因为如果需要,您可以多次实例化类,并且无法从任何地方获取实例。但是,如果您仅通过工厂/容器创建实例,则最终会在代码中获得该类的唯一实例。==注意==:我认为Spring框架非常令人困惑,因为它的“单例”范围只是一个实例。我花了一些时间才明白,这不是一个真正的GoF的单例。## 一些想法当涉及到全局状态时,单个实例具有与单例相同的缺点。**您应该避免使用单个实例在不同类之间共享数据!** 我看到的唯一例外是缓存:- 想象一下,您有一个交易应用程序,每秒进行数百次调用,它只需要具有最后几分钟的股票价格。您可以使用在交易业务类之间共享的单个实例 (StockPriceManager),每个需要价格的函数都将从缓存中获取它。如果价格已过时,缓存将刷新它。在这种情况下,紧密耦合的缺点值得在性能上获得收益。但是,当你因为这个全局状态而需要理解生产中的错误时,你会哭泣(我去过那里,这并不好笑)。我告诉您使用单实例方法而不是单例,但有时当您在所有类中都需要此对象时,值得使用真正的单例。例如,当您需要记录以下内容时:- 每个类都需要记录,并且此日志类通常是唯一的(因为日志写在同一个文件中)。由于所有类都使用 log 类,因此您知道每个类都与此日志类具有隐式依赖关系。此外,这不是业务需求,因此对日志进行单元测试“不太重要”(我感到羞耻)。编写单例比使用依赖关系注入编写单个实例更容易。对于快速而肮脏的解决方案,我将使用单例。对于一个长期耐用的解决方案,我将使用单个实例。由于大多数应用程序都基于框架,因此单个实例的实现比从头开始更容易(假设您知道如何使用框架)。如果您想了解有关单例的更多信息:- 在 stackexchange 上,关于[单例模式的缺点](https://programmers.stackexchange.com/questions/40373/so-singletons-are-bad-then-what)也有一个非常好的答案。## 真实示例单实例模式使用工厂。如果使用可实例化工厂,则可能需要确保此工厂是唯一的。更广泛地说,当您使用工厂时,您可能希望它是唯一的,以避免2个工厂实例相互混淆。你可以使用“元工厂”来构建唯一的工厂,但你最终会遇到“元工厂”的相同问题。因此,执行此操作的唯一方法是使用单例创建工厂。旧图形库AWT中的[java.awt.Toolkit](https://docs.oracle.com/javase/8/docs/api/java/awt/Toolkit.html)就是这种情况。此类提供了一个 getDefaultToolkit() 方法,该方法提供唯一的 Toolkit 实例,这是获取实例的唯一方法。使用此工具包(这是一个工厂),您可以创建一个窗口,一个按钮,一个复选框...但是,您也可以遇到单例以解决其他问题。当您需要用Java监视系统时,必须使用java.lang.Runtime类。我想这个类必须是唯一的,因为它表示进程的全局状态(环境变量)。如果我引用[java API](https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html):> “每个 Java 应用程序都有一个类 Runtime 实例,该实例允许应用程序与运行应用程序的环境进行交互。当前运行时可以从 **getRuntime** 方法获取。# 原型模式我在整个春季都使用过原型,但我从来没有需要使用自己的原型。此模式旨在通过复制而不是构造函数来构建对象。以下是GoF给出的定义:“使用原型实例指定要创建的对象类型,并通过复制此原型来创建新对象。和GoF一样,我不理解他们的句子(这是因为英语不是我的母语吗?)。如果你像我一样,这里有另一个解释:如果你不想或不能使用类的构造函数,原型模式允许你通过复制已经存在的实例来创建这个类的新实例。让我们看一下使用UML图的正式定义:[![原型模式](https://img-blog.csdnimg.cn/img_convert/7bd597f77b6b81242842d2916ba75acc.png)](http://coding-geek.com/wp-content/uploads/2015/06/prototype.png)在此图中- 原型是一个定义函数 clone() 的接口- 一个真正的原型必须实现这个接口,并实现 clone() 函数来返回自身的副本。开发人员必须实例化一次 ConcretePrototype。然后,他将能够通过以下方式创建ConcretePrototype的新实例:- 使用 clone() 函数复制第一个实例- 或者使用(再次)构造函数创建混凝土原型。## 何时使用原型?根据Gof的说法,应该使用原型:- 当系统应该独立于其产品的创建,组合和表示方式- 当要实例化的类是在运行时指定的,例如,通过动态加载- 以避免构建与产品类层次结构平行的工厂类层次结构- 当类的实例可以具有只有几种不同的状态组合之一时。安装相应数量的原型并克隆它们可能比每次都使用适当的状态手动实例化类更方便。未知类的动态加载是非常罕见的情况,如果需要复制动态加载的实例,则更是如此。这本书写于1994年。现在,你可以通过使用依赖注入来“避免构建工厂的类层次结构”(同样,我将在以后的文章中介绍这个奇妙的模式)。在我看来,最常见的情况是创建有状态实例比复制现有实例更昂贵,并且您需要创建大量此对象。例如,如果创建需要:- 从数据库连接获取数据,- 从系统(通过系统调用)或文件系统获取数据,- 从另一台服务器获取数据(使用套接字,Web服务或其他),- 计算大量数据(例如,如果需要对数据进行排序),- 做任何需要时间的事情。该对象必须是有状态的,因为如果它没有状态,则 Singleton(或单个实例)将完成该操作。还有另一个用例。如果您有一个可变的实例,并且您希望将其提供给代码的另一部分,出于安全原因,您可能希望提供副本而不是真实实例,因为客户端代码可以修改此实例,并对使用它的代码的其他部分产生影响。## Java 实现让我们看一个Java中的简单示例:- 我们有一个汽车比较器商务舱。此类包含比较 2 辆车的函数。- 要实例化 CarComparator,构造函数需要从数据库中加载默认配置来配置汽车比较算法(例如,在油耗上比速度或价格更重要)。- 此类不能是单一实例,因为配置可以由每个用户修改(因此每个用户都需要自己的实例)。- 这就是为什么我们只使用昂贵的构造函数创建一次实例。- 然后,当客户需要 CarComparator 的实例时,他将获得第一个实例的副本。```java//////////////////////////The Prototype interfacepublic interface Prototype {    Prototype duplicate();}//////////////////////////The class we want to duplicatepublic class CarComparator implements Prototype{    private int priceWeigth;    private int speedWeigth;    private int fuelConsumptionWeigth;    //a constructor that makes costly calls to a database    //to get the default weigths    public CarComparator(DatabaseConnection connect){        //I let you imagine the costly calls to the database    }    //A private constructor only use to duplicate the object    private CarComparator(int priceWeigth,int speedWeigth,int fuelConsumptionWeigth){        this.priceWeigth=priceWeigth;        this.speedWeigth=speedWeigth;        this.fuelConsumptionWeigth=fuelConsumptionWeigth;    }    //The prototype method    @Override    public Prototype duplicate() {        return new CarComparator(priceWeigth, speedWeigth, fuelConsumptionWeigth);    }    int compareCars(Car first, Car second){        //some kickass and top secret algorithm using the weigths    }    ////////////////The setters that lets the possibility to modify    //////////////// the algorithm behaviour    public void setPriceWeigth(int priceWeigth) {        this.priceWeigth = priceWeigth;    }    public void setSpeedWeigth(int speedWeigth) {        this.speedWeigth = speedWeigth;    }    public void setFuelConsumptionWeigth(int fuelConsumptionWeigth) {        this.fuelConsumptionWeigth = fuelConsumptionWeigth;    }////////////////////////// A factory that creates a CarComparator instance using////////////////////////// constructors then it creates the others by duplication.////////////////////////// When a client ask for a CarComparator////////////////////////// he gets a duplicatepublic class CarComparatorFactory {    CarComparator carComparator;    public BusinessClass (DatabaseConnection connect) {        //We create one instance of CarComparator        carComparator = new CarComparator(connect);    }    //we duplicate the instance so that    //the duplicated instances can be modified    public CarComparator getCarComparator(){        return carComparator.duplicate();    }}```如果你看下一部分,你会发现我本可以使用正确的Java接口制作一个更简单的代码,但我希望你理解一个原型。在此示例中,在启动时,将使用数据库中的默认配置创建原型,并且每个客户端将使用工厂的 getCarComparator() 方法获取此实例的副本。## **真实示例**Java API提供了一个名为[Cloneable的](https://docs.oracle.com/javase/8/docs/api/java/lang/Cloneable.html)原型接口。此接口定义了一个 clone() 函数,具体原型需要实现该函数。Java API 中的许多 Java 类都实现了此接口,例如来自集合 API 的集合。使用ArrayList,我可以克隆它并获取一个新的ArrayList,其中包含与原始数组相同的数据:```java// Let's initialize a list// with 10  integersArrayList<Integer> list = new ArrayList<Integer>();for (int i = 0; i < 10; i++) {   list.add(i);}System.out.println("content of the list "+list);//Let's now duplicate the list using the prototype methodArrayList<Integer> duplicatedSet = (ArrayList<Integer>) list.clone();System.out.println("content of the duplicated list "+duplicatedSet);```此代码的结果是:> 集合的内容[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]> 重复集合的内容[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]# 生成器模式生成器模式对于分解代码非常有用。根据GoF,这种模式:*“将复杂对象的构造与其表示分开,以便相同的构造过程可以创建不同的表示。*它的目标随着时间的推移而变化,大多数时候它被用来避免创建许多构造函数,这些构造函数仅因参数数量而异。这是一种避免**伸缩构造函数反模式**的方法。## 它解决的问题让我们看一下此模式解决的问题。想象一个具有5个属性的类人:- 年龄- 重量- 高度- 编号- 名字我们希望能够构建一个人知道:- 只有这个年龄,- 或只有这个年龄和体重,- 或只有这个年龄,体重和身高,- 或仅此年龄,体重,身高和ID- 或仅此年龄,体重,身高,ID和姓名在java中,我们可以写这样的东西。```javapublic class Person {    private int age;    private int weigth;    private int height;    private int id;    private String name;    //////////////Here comes the telescopic constructors    public Person() {        //some stuff    }    public Person(int age) {        this();//we're using the previous constructor        this.age = age;    }    public Person(int age, int weigth) {        this(age);//we're using the previous constructor        this.weigth = weigth;    }    public Person(int age, int weigth, int height) {        this(age, weigth);//we're using the previous constructor        this.height= height;    }      public Person(int age, int weigth, int height,int id) {        this(age, weigth, height);//we're using the previous constructor        this.id = id;    }      public Person(int age, int weigth, int height,int id,String name) {        this(age, weigth, height, id);//we're using the previous constructor        this.name = name;    }   }```为了处理这个简单的需求,我们刚刚创建了5个构造函数,其中有很多代码。我知道java是一种非常冗长的语言(内部巨魔),但是如果有一种更干净的方式呢?此外,使用这种伸缩式方法,代码很难阅读。例如,如果您阅读以下代码,您是否可以轻松理解参数是什么?他们是年龄,身份还是身高?```javaPerson person1 = new Person (45, 45, 160, 1);Person person2 = new Person (45, 170, 150);```下一个问题:假设你现在希望能够创建一个包含你获得的每一个可能的信息部分的人,例如:- 一个年龄- 重量- 年龄和体重- 一个年龄和一个ID,- 一个年龄,一个体重和一个名字,- ...使用构造函数方法,您最终将获得 120 个构造函数 (5! = 120)。你还有另一个问题,如何处理使用相同类型的不同构造函数?例如,您如何同时拥有两者:- age 和 weigth(为 2 int)的构造函数,以及- 年龄和 id 的构造函数(也是 2 int)?您可以使用静态工厂方法,但它仍然需要 120 个静态工厂方法。**这就是建筑商发挥作用的地方!**此模式的思想是模拟**命名的可选参数**。这些类型的参数在某些语言(如python)中是本地可用的。由于UML版本非常复杂(我认为),我们将从一个简单的java示例开始,并以UML正式定义结束。## 一个简单的 java 示例在此示例中,我有一个人,但这次 id 字段是必填字段,其他字段是可选的。我创建了一个构建器,以便开发人员可以根据需要使用可选字段。```java//////////////////////the person class/////////////////////if you look at its constructor/////////////////////it requieres a builderpublic class Person {   private final int id;// mandatory   private int weigth;// optional   private int height;// optional   private int age;// optional   private String name;// optional   public Person(PersonBuilder builder) {       age = builder.age;       weigth = builder.weigth;       height = builder.height;       id = builder.id;       name = builder.name;    }}//////////////////////the builder that/////////////////////takes care of/////////////////////Person creationpublic class PersonBuilder {    // Required parameters    final int id;    // Optional parameters - initialized to default values        int height;    int age;    int weigth;    String name = "";    public PersonBuilder(int id) {        this.id = id;    }    public PersonBuilder age(int val) {        age = val;        return this;    }    public PersonBuilder weigth(int val) {        weigth = val;        return this;    }    public PersonBuilder height(int val) {        height = val;        return this;    }    public PersonBuilder name(String val) {        name = val;        return this;    }    public Person build() {        return new Person(this);    }}//////////////////////Here is how to use the builder in order to build a Person//////////////////////You can see how readable is the codepublic class SomeClass {    public void someMethod(int id){        PersonBuilder pBuilder = new PersonBuilder(id);        Person robert = pBuilder.name("Robert").age(18).weigth(80).build();        //some stuff    }    public void someMethodBis(int id){        PersonBuilder pBuilder = new PersonBuilder(id);        Person jennifer = pBuilder.height(170).name("Jennifer").build();        //some stuff    }}```在此示例中,我假设类 Person 和 PersonBuilder 位于同一个包中,这允许构建器使用 Person 构造函数,并且包外的类必须使用 PersonBuilder 来创建 Person。这个PersonBuilder有2种方法,一种用于构建人的**一部分**,另一种用于**创建**人。一个人的所有属性只能由同一包中的类修改。我应该使用getter和setter,但我想有一个简短的例子。您看到使用构建器的部分易于阅读,我们知道我们正在创建- 一个名叫罗伯特的人,他18岁,体重80岁,- 另一个名叫詹妮弗的人,她长170岁。这种技术的另一个优点是,您仍然可以创建**不可变的对象**。在我的示例中,如果我不在 Person 类中添加公共 setter,则 Person 实例是不可变的,因为包外部的任何类都不能修改其属性。## 正式定义现在让我们看一下UML:[![GoF 的生成器模式](https://img-blog.csdnimg.cn/img_convert/5a6bc67754bc268fc56ce2648dabcd1a.png)](http://coding-geek.com/wp-content/uploads/2015/06/builder.png)这张图真的很抽象,GoF的构建器有:- 一个生成器界面,用于指定用于创建 Product 对象的部件的函数。在我的图中,只有一个方法,buildPart()。- 一个 ConcreteBuilder,它通过实现 Builder 接口来构造和组装产品的部件。- 控制器:它使用生成器界面构造产品。根据 GoF,此模式在以下情况下很有用:> • 创建复杂对象的算法应独立于构成对象的部件及其组装方式。> •构造过程必须允许对构造的对象进行不同的表示。GoF给出的例子是一个TextConverter构建器,它有3个实现要构建:ASCIIText或TeXText或TextWidget。3 个生成器实现(ASCIIConverter、TeXConverter 和 TextWidgetConverter)具有相同的函数,除了 createObject() 函数不同(这就是为什么此函数不在此模式的接口中)。使用此模式,转换文本的代码(控制器)使用生成器界面,因此它可以轻松地从 ASCII 切换到 TeX 或 TextWidget。此外,您可以添加新的转换器,而无需修改其余代码。在某种程度上,这种模式非常接近国家模式。**但这个问题是罕见的。**这种模式的另一个用途是由Java开发人员Joshua Bloch推广的,他领导了许多Java API的构建。他在《*有效的Java》*一书中写道:> “在面对许多构造函数参数时考虑构建器”**大多数情况下,该模式用于此用例**。对于此问题,您不需要生成器接口、多个生成器实现或控制器。在我的java示例中,大多数时候你会发现只有一个具体的构建器。然后,UML 变得更加容易:[![约书亚·布洛赫的建造者模式](https://img-blog.csdnimg.cn/img_convert/910ae5ea7a032bb32f297d5f19223634.png)](http://coding-geek.com/wp-content/uploads/2015/06/builder2.png)在此图中,ConcreteBuilder具有多个函数来创建产品的每个部分(但我只是放了一个,buildPart(),因为我懒惰)。这些函数返回 ConcreteBuilder,以便您可以链接函数调用,例如:builder.buildPart1().buildPart7().createObject()。构建器有一个 createObject() 方法,用于在您不需要添加更多部件时创建产品。总而言之,当您的类具有许多可选参数并且不希望最终得到许多构造函数时,构建器模式是一个不错的选择。虽然这个模式不是为这个问题而设计的,但它大部分时间都用于这个问题(至少在Java中)。## 真实示例Java API中最常见的例子是[StringBuilder](https://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html)。使用它,您可以创建一个临时字符串,向其追加新字符串,完成后,您可以创建一个真正的String对象(这是不可变的)。```javaStringBuilder sBuilder = new StringBuilder();String example = sBuilder.append("this").append(" is").   append(" an").append(" example").toString();System.out.println(example);```# 结论您现在应该对创建模式有了更好的了解。如果您需要记住一件事,那就是使用单个实例而不是单例。请记住生成器模式(Joshua Bloch的版本),如果您正在处理可选参数,它可能会很有用。
文章
设计模式  ·  SQL  ·  缓存  ·  Java  ·  数据库连接  ·  测试技术  ·  API  ·  uml  ·  容器  ·  Spring
2022-06-28
「Spring」Boot Docker 认证指南(下)
构建插件如果你不想docker在你的构建中直接调用,有一套丰富的 Maven 和 Gradle 插件可以为你完成这项工作。这里仅仅是少数。Spring Boot Maven 和 Gradle 插件您可以使用Maven和Gradle的 Spring Boot 构建插件来创建容器映像。docker build这些插件使用Cloud Native Buildpacks创建一个 OCI 映像(与创建的格式相同) 。您不需要Dockerfile,但您确实需要 Docker 守护程序,可以在本地(使用 docker 构建时使用)或通过DOCKER_HOST环境变量远程进行。默认构建器针对 Spring Boot 应用程序进行了优化,并且图像像上面的示例一样有效地分层。以下示例在不更改pom.xml文件的情况下使用 Maven:./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myorg/myapp复制以下示例适用于 Gradle,无需更改build.gradle文件:./gradlew bootBuildImage --imageName=myorg/myapp复制第一次构建可能需要很长时间,因为它必须下载一些容器镜像和 JDK,但后续构建应该很快。然后您可以运行映像,如以下清单所示(带输出):docker run -p 8080:8080 -t myorg/myappSetting Active Processor Count to 6Calculating JVM memory based on 14673596K available memoryCalculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx14278122K -XX:MaxMetaspaceSize=88273K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 14673596K, Thread Count: 50, Loaded Class Count: 13171, Headroom: 0%)Adding 129 container CA certificates to JVM truststoreSpring Cloud Bindings EnabledPicked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -agentpath:/layers/paketo-buildpacks_bellsoft-liberica/jvmkill/jvmkill-1.16.0-RELEASE.so=printHeapHistogram=1 -XX:ActiveProcessorCount=6 -XX:MaxDirectMemorySize=10M -Xmx14278122K -XX:MaxMetaspaceSize=88273K -XX:ReservedCodeCacheSize=240M -Xss1M -Dorg.springframework.cloud.bindings.boot.enable=true....2015-03-31 13:25:48.035 INFO 1 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)2015-03-31 13:25:48.037 INFO 1 --- [ main] hello.Application复制您可以看到应用程序正常启动。您可能还注意到 JVM 内存需求是在容器内计算并设置为命令行选项的。这与多年来在 Cloud Foundry 构建包中使用的内存计算相同。它代表了对一系列 JVM 应用程序(包括但不限于 Spring Boot 应用程序)的最佳选择的重要研究,结果通常比 JVM 的默认设置好得多。您可以自定义命令行选项并通过设置环境变量覆盖内存计算器,如Paketo buildpacks 文档中所示。Spotify Maven 插件Spotify Maven 插件是一个受欢迎的选择。它要求您编写 aDockerfile然后docker为您运行,就像您在命令行上执行它一样。docker 镜像标签和其他东西有一些配置选项,但它使您的应用程序中的 docker 知识集中在一个Dockerfile很多人喜欢的 .对于真正的基本用法,它无需额外配置即可开箱即用:mvn com.spotify:dockerfile-maven-plugin:build...[INFO] Building Docker context /home/dsyer/dev/demo/workspace/myapp[INFO][INFO] Image will be built without a name[INFO]...[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 7.630 s[INFO] Finished at: 2018-11-06T16:03:16+00:00[INFO] Final Memory: 26M/595M[INFO] ------------------------------------------------------------------------复制这将构建一个匿名 docker 映像。我们现在可以在命令行上标记它docker或使用 Maven 配置将其设置为repository. 以下示例在不更改pom.xml文件的情况下工作:$ mvn com.spotify:dockerfile-maven-plugin:build -Ddockerfile.repository=myorg/myapp复制或者,您更改pom.xml​文件:pom.xml<plugins> <plugin> <groupId>com.spotify</groupId> <artifactId>dockerfile-maven-plugin</artifactId> <version>1.4.8</version> <configuration> <repository>myorg/${project.artifactId}</repository> </configuration> </plugin> </plugins>复制Palantir Gradle 插件Palantir Gradle 插件与 a 一起使用,Dockerfile并且还可Dockerfile以为您生成 a。然后它docker就像在命令行上运行它一样运行。首先,您需要将插件导入您的build.gradle:build.gradlebuildscript {... dependencies { ... classpath('gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0') }}复制然后,最后,您可以应用插件并调用它的任务:build.gradleapply plugin: 'com.palantir.docker'group = 'myorg'bootJar {baseName = 'myapp' version = '0.1.0'}task unpack(type: Copy) {dependsOn bootJar from(zipTree(tasks.bootJar.outputs.files.singleFile)) into("build/dependency")}docker {name "${project.group}/${bootJar.baseName}" copySpec.from(tasks.unpack.outputs).into("dependency") buildArgs(['DEPENDENCY': "dependency"])}复制在本例中,我们选择将 Spring Boot fat JAR 解压到build目录中的特定位置,该位置是 docker build 的根目录。Dockerfile然后早期显示的多层(不是多阶段)起作用。Jib Maven 和 Gradle 插件Google 有一个名为Jib的开源工具,它相对较新,但出于多种原因非常有趣。可能最有趣的是您不需要 docker 来运行它。Jib 使用与您获得的相同标准输出来构建映像,docker build但除非您要求它,否则它不会使用docker,因此它可以在未安装 docker 的环境中工作(在构建服务器中很常见)。您也不需要Dockerfile(无论如何都会被忽略)或任何东西pom.xml来获得在 Maven 中构建的图像(Gradle 将要求您至少在 中安装插件build.gradle)。DockerfileJib 的另一个有趣的特性是它对层有意见,并且它以与上面创建的多层略有不同的方式优化它们。与胖 JAR 中一样,Jib 将本地应用程序资源与依赖项分开,但它更进一步,还将快照依赖项放入单独的层,因为它们更有可能发生变化。有用于进一步自定义布局的配置选项。以下示例在不更改 Maven 的情况下使用pom.xml:$ mvn com.google.cloud.tools:jib-maven-plugin:build -Dimage=myorg/myapp复制myorg要运行该命令,您需要具有在存储库前缀下推送到 Dockerhub 的权限。如果您已docker在命令行上进行了身份验证,则可以在本地~/.docker配置中使用。~/.m2/settings.xml您还可以在您的(id存储库的重要)中设置 Maven“服务器”身份验证:settings.xml<server> <id>registry.hub.docker.com</id> <username>myorg</username> <password>...</password> </server>复制还有其他选项——例如,您可以docker使用dockerBuild目标而不是build. 还支持其他容器注册表。对于每一项,您都需要通过 Docker 或 Maven 设置来设置本地身份验证。gradle 插件具有类似的功能,一旦你在你的build.gradle:.build.gradleplugins { ... id 'com.google.cloud.tools.jib' version '1.8.0'}复制以下清单使用入门指南中使用的旧 Gradle 样式:build.gradlebuildscript {repositories { maven { url "https://plugins.gradle.org/m2/" } mavenCentral() } dependencies { classpath('org.springframework.boot:spring-boot-gradle-plugin:2.2.1.RELEASE') classpath('com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin:1.8.0') }}复制然后,您可以通过运行以下命令来构建映像:./gradlew jib --image=myorg/myapp复制与 Maven 构建一样,如果您已docker在命令行上进行了身份验证,则图像推送将从您的本地~/.docker配置进行身份验证。持续集成如今,自动化(或应该是)是每个应用程序生命周期的一部分。人们用来进行自动化的工具往往非常擅长从源代码调用构建系统。因此,如果这为您提供了一个 docker 映像,并且构建代理中的环境与开发人员自己的环境充分一致,那可能就足够了。对 docker 注册表进行身份验证可能是最大的挑战,但所有自动化工具中都有一些功能可以帮助解决这个问题。但是,有时最好将容器创建完全留给自动化层,在这种情况下,可能不需要污染用户的代码。容器创建很棘手,开发人员有时不需要真正关心它。如果用户代码更干净,则不同的工具更有可能“做正确的事”(应用安全修复、优化缓存等)。自动化有多种选择,如今它们都带有一些与容器相关的功能。我们将看一对夫妇。大厅Concourse是一个基于管道的自动化平台,可用于 CI 和 CD。它在 VMware 内部使用,该项目的主要作者在那里工作。Concourse 中的所有内容都是无状态的,并且在容器中运行,CLI 除外。由于运行容器是自动化管道的主要业务顺序,因此很好地支持创建容器。Docker Image Resource负责保持构建的输出状态是最新的,如果它是一个容器镜像的话。以下示例管道为前面显示的示例构建了一个 docker 映像,假设它位于 github 中myorg/myapp,Dockerfile在根中有一个,并且在 中有一个构建任务声明src/main/ci/build.yml:resources:name: myapptype: gitsource: uri: https://github.com/myorg/myapp.gitname: myapp-imagetype: docker-imagesource: email: {{docker-hub-email}} username: {{docker-hub-username}} password: {{docker-hub-password}} repository: myorg/myappjobs:name: mainplan:task: buildfile: myapp/src/main/ci/build.ymlput: myapp-imageparams: build: myapp复制管道的结构是非常具有声明性的:您定义“资源”(输入、输出或两者)和“作业”(使用资源并将操作应用于资源)。如果任何输入资源发生更改,则会触发新的构建。如果任何输出资源在作业期间发生更改,则会对其进行更新。管道可以在与应用程序源代码不同的地方定义。此外,对于通用构建设置,任务声明也可以集中或外部化。这允许在开发和自动化之间分离一些关注点,这适合一些软件开发组织。詹金斯Jenkins是另一个流行的自动化服务器。它具有大量功能,但最接近此处其他自动化示例的是管道功能。下面Jenkinsfile使用 Maven 构建一个 Spring Boot 项目,然后使用 aDockerfile构建一个镜像并将其推送到存储库:Jenkinsfilenode {checkout scm sh './mvnw -B -DskipTests clean package' docker.build("myorg/myapp").push()}复制对于需要在构建服务器中进行身份验证的(实际)docker 存储库,您可以docker使用docker.withCredentials(…​).构建包packSpring Boot Maven 和 Gradle 插件使用构建包的方式与CLI 在以下示例中的使用方式完全相同。给定相同的输入,生成的图像是相同的。Cloud Foundry在内部使用容器已经很多年了,用于将用户代码转换为容器的部分技术是 Build Packs,这个想法最初是从Heroku借来的。当前一代的 buildpacks (v2) 生成由平台组装到容器中的通用二进制输出。新一代构建包(v3) 是 Heroku 与其他公司(包括 VMware)的合作,它直接明确地构建容器镜像。这对开发人员和运营商来说很有趣。开发人员不需要太关心如何构建容器的细节,但如果需要,他们可以轻松创建一个。Buildpacks 还具有许多用于缓存构建结果和依赖项的功能。通常,构建包的运行速度比原生 Docker 构建快得多。操作员可以扫描容器以审核其内容并将其转换为修补它们以进行安全更新。此外,您可以在本地(例如,在开发人员机器或 CI 服务中)或在 Cloud Foundry 等平台中运行构建包。buildpack 生命周期的输出是容器映像,但您不需要Dockerfile. 输出映像中的文件系统层由 buildpack 控制。通常,许多优化都是在开发人员不必知道或关心它们的情况下进行的。在较低层(例如包含操作系统的基础映像)和较高层(包含中间件和语言特定依赖项)之间还有一个应用程序二进制接口。这使得 Cloud Foundry 等平台可以在有安全更新的情况下修补较低层,而不会影响应用程序的完整性和功能。为了让您了解 buildpack 的功能,以下示例(显示其输出)从命令行使用Pack CLI(它可以与我们在本指南中使用的示例应用程序一起使用 - 不需要Dockerfile或任何特殊的构建配置):pack build myorg/myapp --builder=paketobuildpacks/builder:base --path=.base: Pulling from paketobuildpacks/builderDigest: sha256:4fae5e2abab118ca9a37bf94ab42aa17fef7c306296b0364f5a0e176702ab5cbStatus: Image is up to date for paketobuildpacks/builder:basebase-cnb: Pulling from paketobuildpacks/runDigest: sha256:a285e73bc3697bc58c228b22938bc81e9b11700e087fd9d44da5f42f14861812Status: Image is up to date for paketobuildpacks/run:base-cnb===> DETECTING7 of 18 buildpacks participatingpaketo-buildpacks/ca-certificates 2.3.2paketo-buildpacks/bellsoft-liberica 8.2.0paketo-buildpacks/maven 5.3.2paketo-buildpacks/executable-jar 5.1.2paketo-buildpacks/apache-tomcat 5.6.1paketo-buildpacks/dist-zip 4.1.2paketo-buildpacks/spring-boot 4.4.2===> ANALYZINGPrevious image with name "myorg/myapp" not found===> RESTORING===> BUILDINGPaketo CA Certificates Buildpack 2.3.2https://github.com/paketo-buildpacks/ca-certificates Launch Helper: Contributing to layerCreating /layers/paketo-buildpacks_ca-certificates/helper/exec.d/ca-certificates-helper Paketo BellSoft Liberica Buildpack 8.2.0https://github.com/paketo-buildpacks/bellsoft-liberica Build Configuration:$BP_JVM_VERSION 11 the Java versionLaunch Configuration:$BPL_JVM_HEAD_ROOM 0 the headroom in memory calculation $BPL_JVM_LOADED_CLASS_COUNT 35% of classes the number of loaded classes in memory calculation $BPL_JVM_THREAD_COUNT 250 the number of threads in memory calculation $JAVA_TOOL_OPTIONS the JVM launch flagsBellSoft Liberica JDK 11.0.12: Contributing to layerDownloading from https://github.com/bell-sw/Liberica/releases/download/11.0.12+7/bellsoft-jdk11.0.12+7-linux-amd64.tar.gz Verifying checksum Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jdk Adding 129 container CA certificates to JVM truststore Writing env.build/JAVA_HOME.override Writing env.build/JDK_HOME.overrideBellSoft Liberica JRE 11.0.12: Contributing to layerDownloading from https://github.com/bell-sw/Liberica/releases/download/11.0.12+7/bellsoft-jre11.0.12+7-linux-amd64.tar.gz Verifying checksum Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jre Adding 129 container CA certificates to JVM truststore Writing env.launch/BPI_APPLICATION_PATH.default Writing env.launch/BPI_JVM_CACERTS.default Writing env.launch/BPI_JVM_CLASS_COUNT.default Writing env.launch/BPI_JVM_SECURITY_PROVIDERS.default Writing env.launch/JAVA_HOME.default Writing env.launch/MALLOC_ARENA_MAX.defaultLaunch Helper: Contributing to layerCreating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/active-processor-count Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/java-opts Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/link-local-dns Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/openssl-certificate-loader Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-configurer Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-classpath-9JVMKill Agent 1.16.0: Contributing to layerDownloading from https://github.com/cloudfoundry/jvmkill/releases/download/v1.16.0.RELEASE/jvmkill-1.16.0-RELEASE.so Verifying checksum Copying to /layers/paketo-buildpacks_bellsoft-liberica/jvmkill Writing env.launch/JAVA_TOOL_OPTIONS.append Writing env.launch/JAVA_TOOL_OPTIONS.delimJava Security Properties: Contributing to layerWriting env.launch/JAVA_SECURITY_PROPERTIES.default Writing env.launch/JAVA_TOOL_OPTIONS.append Writing env.launch/JAVA_TOOL_OPTIONS.delim Paketo Maven Buildpack 5.3.2https://github.com/paketo-buildpacks/maven Build Configuration:$BP_MAVEN_BUILD_ARGUMENTS -Dmaven.test.skip=true package the arguments to pass to Maven $BP_MAVEN_BUILT_ARTIFACT target/*.[jw]ar the built application artifact explicitly. Supersedes $BP_MAVEN_BUILT_MODULE $BP_MAVEN_BUILT_MODULE the module to find application artifact in Creating cache directory /home/cnb/.m2Compiled Application: Contributing to layerExecuting mvnw --batch-mode -Dmaven.test.skip=true package [ ... Maven build output ... ][INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 53.474 s[INFO] Finished at: 2021-07-23T20:10:28Z[INFO] ------------------------------------------------------------------------ Removing source codePaketo Executable JAR Buildpack 5.1.2https://github.com/paketo-buildpacks/executable-jar Class Path: Contributing to layerWriting env/CLASSPATH.delim Writing env/CLASSPATH.prependProcess types:executable-jar: java org.springframework.boot.loader.JarLauncher (direct) task: java org.springframework.boot.loader.JarLauncher (direct) web: java org.springframework.boot.loader.JarLauncher (direct) Paketo Spring Boot Buildpack 4.4.2https://github.com/paketo-buildpacks/spring-boot Creating slices from layers indexdependencies spring-boot-loader snapshot-dependencies applicationLaunch Helper: Contributing to layerCreating /layers/paketo-buildpacks_spring-boot/helper/exec.d/spring-cloud-bindingsSpring Cloud Bindings 1.7.1: Contributing to layerDownloading from https://repo.spring.io/release/org/springframework/cloud/spring-cloud-bindings/1.7.1/spring-cloud-bindings-1.7.1.jar Verifying checksum Copying to /layers/paketo-buildpacks_spring-boot/spring-cloud-bindingsWeb Application Type: Contributing to layerReactive web application detected Writing env.launch/BPL_JVM_THREAD_COUNT.default4 application slices Image labels:org.opencontainers.image.title org.opencontainers.image.version org.springframework.boot.version===> EXPORTINGAdding layer 'paketo-buildpacks/ca-certificates:helper'Adding layer 'paketo-buildpacks/bellsoft-liberica:helper'Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'Adding layer 'paketo-buildpacks/bellsoft-liberica:jre'Adding layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'Adding layer 'paketo-buildpacks/executable-jar:classpath'Adding layer 'paketo-buildpacks/spring-boot:helper'Adding layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'Adding layer 'paketo-buildpacks/spring-boot:web-application-type'Adding 5/5 app layer(s)Adding layer 'launcher'Adding layer 'config'Adding layer 'process-types'Adding label 'io.buildpacks.lifecycle.metadata'Adding label 'io.buildpacks.build.metadata'Adding label 'io.buildpacks.project.metadata'Adding label 'org.opencontainers.image.title'Adding label 'org.opencontainers.image.version'Adding label 'org.springframework.boot.version'Setting default process type 'web'Saving myorg/myapp...* Images (ed1f92885df0): myorg/myappAdding cache layer 'paketo-buildpacks/bellsoft-liberica:jdk'Adding cache layer 'paketo-buildpacks/maven:application'Adding cache layer 'paketo-buildpacks/maven:cache'Successfully built image 'myorg/myapp'复制这--builder是一个运行 buildpack 生命周期的 Docker 镜像。通常,它将是所有开发人员或单个平台上的所有开发人员的共享资源。您可以在命令行上设置默认构建器(在 中创建一个文件~/.pack),然后从后续构建中省略该标志。构建器paketobuildpacks/builder:base还知道如何从可执行 JAR 文件构建映像,因此您可以先使用 Maven 构建,然后将其指向--pathJAR 文件以获得相同的结果。原生容器和平台领域的另一个新项目是Knative。如果您不熟悉它,可以将其视为构建无服务器平台的构建块。它建立在Kubernetes 之上,因此最终它会使用容器镜像并将它们转化为平台上的应用程序或“服务”。不过,它的主要功能之一是能够使用源代码并为您构建容器,使其对开发人员和操作员更加友好。Knative Build是执行此操作的组件,它本身就是一个灵活的平台,用于将用户代码转换为容器——您几乎可以以任何您喜欢的方式进行操作。一些模板提供了通用模式(例如 Maven 和 Gradle 构建)和多阶段 docker 构建使用卡尼科。还有一个模板使用了Buildpacks,这对我们来说很有趣,因为 buildpacks 一直对 Spring Boot 有很好的支持。结束本指南提供了许多用于为 Spring Boot 应用程序构建容器映像的选项。所有这些都是完全有效的选择,现在由您决定您需要哪一个。您的第一个问题应该是“我真的需要构建容器映像吗?” 如果答案是“是”,那么您的选择可能会受到效率、可缓存性和关注点分离的驱动。您是否想让开发人员无需过多了解容器镜像的创建方式?当需要修补操作系统和中间件漏洞时,您是否想让开发人员负责更新映像?或者,开发人员可能需要完全控制整个过程,并且他们拥有所需的所有工具和知识。spring认证##java##spring##程序员文末备注:「Spring」Boot Docker 认证指南(下)Spring中国教育管理中心Spring Boot Docker来源:Spring中国教育管理中心
文章
存储  ·  缓存  ·  安全  ·  Java  ·  中间件  ·  Maven  ·  虚拟化  ·  Docker  ·  容器  ·  Spring
2022-06-28
「Spring」Boot Docker 认证指南(上)
许多人使用容器来包装他们的 Spring Boot 应用程序,而构建容器并不是一件简单的事情。这是针对 Spring Boot 应用程序开发人员的指南,容器对于开发人员来说并不总是一个好的抽象。它们迫使你去了解和思考低层次的问题。但是,有时可能会要求您创建或使用容器,因此了解构建块是值得的。在本指南中,我们旨在向您展示如果您面临需要创建自己的容器的前景,您可以做出的一些选择。我们假设您知道如何创建和构建基本的 Spring Boot 应用程序。如果没有,请转到入门指南之一 ——例如,关于构建REST 服务的指南。从那里复制代码并练习本指南中包含的一些想法。还有一个关于Docker的入门指南,这也是一个很好的起点,但它没有涵盖我们在此处介绍的选择范围或详细介绍它们。一个基本的 DockerfileSpring Boot 应用程序很容易转换为可执行的 JAR 文件。所有的入门指南都是这样做的,你从Spring Initializr下载的每个应用程序都有一个构建步骤来创建一个可执行的 JAR。使用 Maven,你运行./mvnw install,使用 Gradle,你运行./gradlew build。运行该 JAR 的基本 Dockerfile 将如下所示,位于项目的顶层:DockerfileFROM openjdk:8-jdk-alpineVOLUME /tmpARG JAR_FILECOPY ${JAR_FILE} app.jarENTRYPOINT ["java","-jar","/app.jar"]复制JAR_FILE您可以作为命令的一部分传入docker(Maven 和 Gradle 不同)。对于 Maven,以下命令有效:docker build --build-arg JAR_FILE=target/*.jar -t myorg/myapp .复制对于 Gradle,以下命令有效:docker build --build-arg JAR_FILE=build/libs/*.jar -t myorg/myapp .复制一旦你选择了一个构建系统,你就不需要ARG. 您可以对 JAR 位置进行硬编码。对于 Maven,如下所示:DockerfileFROM openjdk:8-jdk-alpineVOLUME /tmpCOPY target/*.jar app.jarENTRYPOINT ["java","-jar","/app.jar"]复制然后我们可以使用以下命令构建镜像:docker build -t myorg/myapp .复制然后我们可以通过运行以下命令来运行它:docker run -p 8080:8080 myorg/myapp复制输出类似于以下示例输出:. _ /\ / ' _(_) _ \ \ \ \( ( )\__ | ' | '_| | '_ / _` | \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.0.2.RELEASE)Nov 06, 2018 2:45:16 PM org.springframework.boot.StartupInfoLogger logStartingINFO: Starting Application v0.1.0 on b8469cdc9b87 with PID 1 (/app.jar started by root in /)Nov 06, 2018 2:45:16 PM org.springframework.boot.SpringApplication logStartupProfileInfo...复制如果你想在镜像内部四处寻找,你可以通过运行以下命令在其中打开一个 shell(注意基础镜像没有bash):docker run -ti --entrypoint /bin/sh myorg/myapp复制输出类似于以下示例输出:/ # lsapp.jar dev home media proc run srv tmp varbin etc lib mnt root sbin sys usr/ #我们在示例中使用的 alpine 基础容器没有bash,所以这是一个ashshell。它具有一些但不是全部的特性bash。如果你有一个正在运行的容器并且你想查看它,你可以通过运行docker exec:docker run --name myapp -ti --entrypoint /bin/sh myorg/myappdocker exec -ti myapp /bin/sh/ #复制传递给命令myapp的位置在哪里。如果您没有使用,docker 会分配一个助记名称,您可以从. 您还可以使用容器的 SHA 标识符而不是名称。SHA 标识符在输出中也可见。--namedocker run--namedocker psdocker ps入口点使用Dockerfile的exec 形式ENTRYPOINT,以便没有外壳包装 Java 进程。优点是java进程响应KILL发送到容器的信号。实际上,这意味着(例如)如果您docker run在本地使用图像,则可以使用CTRL-C. 如果命令行有点长,您可以COPY在运行之前将其提取到 shell 脚本中并放入映像中。以下示例显示了如何执行此操作:DockerfileFROM openjdk:8-jdk-alpineVOLUME /tmpCOPY run.sh .COPY target/*.jar app.jarENTRYPOINT ["run.sh"]复制请记住使用exec java …启动 java 进程(以便它可以处理KILL信号):run.sh!/bin/shexec java -jar /app.jar复制入口点的另一个有趣方面是您是否可以在运行时将环境变量注入 Java 进程。例如,假设您想要在运行时添加 Java 命令行选项。您可以尝试这样做:DockerfileFROM openjdk:8-jdk-alpineVOLUME /tmpARG JAR_FILE=target/*.jarCOPY ${JAR_FILE} app.jarENTRYPOINT ["java","${JAVA_OPTS}","-jar","/app.jar"]复制然后您可以尝试以下命令:docker build -t myorg/myapp .docker run -p 9000:9000 -e JAVA_OPTS=-Dserver.port=9000 myorg/myapp复制这失败了,因为${}替换需要一个外壳。exec 表单不使用 shell 来启动进程,因此不应用选项。您可以通过将入口点移动到脚本(如run.sh前面显示的示例)或在入口点显式创建 shell 来解决此问题。以下示例显示了如何在入口点中创建 shell:DockerfileFROM openjdk:8-jdk-alpineVOLUME /tmpARG JAR_FILE=target/*.jarCOPY ${JAR_FILE} app.jarENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]复制然后,您可以通过运行以下命令来启动此应用程序:docker run -p 8080:8080 -e "JAVA_OPTS=-Ddebug -Xmx128m" myorg/myapp复制该命令产生类似于以下的输出:. _ /\ / ' _(_) _ \ \ \ \( ( )\__ | ' | '_| | '_ / _` | \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.0.RELEASE)...2019-10-29 09:12:12.169 DEBUG 1 --- [ main] ConditionEvaluationReportLoggingListener :============================CONDITIONS EVALUATION REPORT...复制(前面的输出显示了 Spring BootDEBUG生成的完整输出的一部分。)-Ddebug将 anENTRYPOINT与显式 shell 一起使用(如前面的示例所做的那样)意味着您可以将环境变量传递给 Java 命令。但是,到目前为止,您还不能为 Spring Boot 应用程序提供命令行参数。以下命令不会在端口 9000 上运行应用程序:docker run -p 9000:9000 myorg/myapp --server.port=9000复制该命令产生以下输出,将端口显示为 8080 而不是 9000:. _ /\ / ' _(_) _ \ \ \ \( ( )\__ | ' | '_| | '_ / _` | \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.0.RELEASE)...2019-10-29 09:20:19.718 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080复制它不起作用,因为 docker 命令(该--server.port=9000部分)被传递到入口点 ( sh),而不是它启动的 Java 进程。要解决此问题,您需要将命令行从以下添加CMD到ENTRYPOINT:DockerfileFROM openjdk:8-jdk-alpineVOLUME /tmpARG JAR_FILE=target/*.jarCOPY ${JAR_FILE} app.jarENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar ${0} ${@}"]复制然后您可以运行相同的命令并将端口设置为 9000:$ docker run -p 9000:9000 myorg/myapp --server.port=9000复制如以下输出示例所示,端口确实设置为 9000:. _ /\ / ' _(_) _ \ \ \ \( ( )\__ | ' | '_| | '_ / _` | \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.0.RELEASE)...2019-10-29 09:30:19.751 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 9000复制注意${0}“命令”(在这种情况下是第一个程序参数)和${@}“命令参数”(程序参数的其余部分)的使用。如果您使用脚本作为入口点,那么您不需要${0}(/app/run.sh在前面的示例中)。以下列表显示了脚本文件中的正确命令:run.sh!/bin/shexec java ${JAVA_OPTS} -jar /app.jar ${@}复制docker配置到现在都非常简单,生成的镜像效率不是很高。docker 镜像有一个文件系统层,其中包含 fat JAR,我们对应用程序代码所做的每一次更改都会更改该层,这可能是 10MB 或更多(对于某些应用程序甚至高达 50MB)。我们可以通过将 JAR 拆分为多个层来改进这一点。较小的图像请注意,前面示例中的基本映像是openjdk:8-jdk-alpine. 这些alpine图像小于Dockerhubopenjdk的标准库图像。您还可以通过使用标签而不是. 并非所有应用程序都使用 JRE(与 JDK 相对),但大多数应用程序都可以。一些组织强制执行一个规则,即每个应用程序都必须使用 JRE,因为存在滥用某些 JDK 功能(例如编译)的风险。jrejdk另一个可以让您获得更小的映像的技巧是使用JLink,它与 OpenJDK 11 捆绑在一起。JLink 允许您从完整 JDK 中的模块子集构建自定义 JRE 分发,因此您不需要 JRE 或 JDK基础图像。原则上,这将使您获得比使用openjdk官方 docker 图像更小的总图像大小。在实践中,您(还)不能将alpine基础镜像与 JDK 11 一起使用,因此您对基础镜像的选择是有限的,并且可能会导致最终镜像的大小更大。此外,您自己的基本映像中的自定义 JRE 不能在其他应用程序之间共享,因为它们需要不同的自定义。因此,您的所有应用程序可能都有较小的图像,但它们仍然需要更长的时间才能启动,因为它们没有从缓存 JRE 层中受益。最后一点突出了图像构建者的一个非常重要的问题:目标不一定总是尽可能地构建最小的图像。较小的图像通常是一个好主意,因为它们需要更少的时间来上传和下载,但前提是它们中的所有图层都没有被缓存。如今,图像注册非常复杂,您很容易通过尝试巧妙地构建图像而失去这些功能的好处。如果您使用通用基础层,图像的总大小就不再那么重要了,而且随着注册中心和平台的发展,它可能变得更不重要。话虽如此,尝试优化应用程序映像中的层仍然很重要且有用。然而,更好的 Dockerfile由于 JAR 本身的打包方式,Spring Boot fat JAR 自然有“层”。如果我们先解包,它已经分为外部依赖和内部依赖。要在 docker 构建中一步完成此操作,我们需要先解压缩 JAR。以下命令(坚持使用 Maven,但 Gradle 版本非常相似)解压缩 Spring Boot fat JAR:mkdir target/dependency(cd target/dependency; jar -xf ../*.jar)docker build -t myorg/myapp .复制然后我们可以使用下面的DockerfileDockerfileFROM openjdk:8-jdk-alpineVOLUME /tmpARG DEPENDENCY=target/dependencyCOPY ${DEPENDENCY}/BOOT-INF/lib /app/libCOPY ${DEPENDENCY}/META-INF /app/META-INFCOPY ${DEPENDENCY}/BOOT-INF/classes /appENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]复制现在有三层,所有应用程序资源都在后面两层。如果应用程序依赖没有改变,第一层(from BOOT-INF/lib)不需要改变,所以构建更快,并且容器在运行时的启动也更快,只要基础层已经被缓存。我们使用了一个硬编码的主应用程序类:hello.Application. 这对于您的应用程序可能有所不同。如果你愿意,你可以用另一个参数化它ARG。您还可以将 Spring Boot fat 复制JarLauncher到映像中并使用它来运行应用程序。它可以工作,您不需要指定主类,但启动时会慢一些。Spring Boot 层索引从 Spring Boot 2.3.0 开始,使用 Spring Boot Maven 或 Gradle 插件构建的 JAR 文件在 JAR 文件中包含层信息。该层信息根据应用程序构建之间更改的可能性来分离应用程序的各个部分。这可以用来使 Docker 镜像层更加高效。层信息可用于将 JAR 内容提取到每个层的目录中:mkdir target/extractedjava -Djarmode=layertools -jar target/*.jar extract --destination target/extracteddocker build -t myorg/myapp .复制然后我们可以使用以下内容Dockerfile:DockerfileFROM openjdk:8-jdk-alpineVOLUME /tmpARG EXTRACTED=/workspace/app/target/extractedCOPY ${EXTRACTED}/dependencies/ ./COPY ${EXTRACTED}/spring-boot-loader/ ./COPY ${EXTRACTED}/snapshot-dependencies/ ./COPY ${EXTRACTED}/application/ ./ENTRYPOINT ["java","org.springframework.boot.loader.JarLauncher"]Spring Boot fatJarLauncher是从 JAR 中提取到镜像中的,因此它可以用于启动应用程序,而无需对主应用程序类进行硬编码。有关使用分层功能的更多信息,请参阅Spring Boot 文档。调整如果您想尽快启动您的应用程序(大多数人都这样做),您可能会考虑一些调整:使用spring-context-indexer(链接到文档)。它不会为小型应用程序增加太多,但每一点都有帮助。如果您负担得起,请不要使用执行器。使用 Spring Boot 2.1(或更高版本)和 Spring 5.1(或更高版本)。使用(通过命令行参数、系统属性或其他方法)修复Spring Boot 配置文件的位置。spring.config.location通过设置来关闭 JMX(您可能不需要在容器中使用它)spring.jmx.enabled=false。使用-noverify. 还要考虑-XX:TieredStopAtLevel=1(这会在以后减慢 JIT 但会缩短启动时间)。使用 Java 8 的容器内存提示:-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap. 在 Java 11 中,默认情况下这是自动的。您的应用程序在运行时可能不需要完整的 CPU,但它确实需要多个 CPU 才能尽快启动(至少两个,四个更好)。如果您不介意启动速度较慢,则可以将 CPU 限制在四个以下。如果您被迫从少于四个 CPU 开始,设置 可能会有所帮助-Dspring.backgroundpreinitializer.ignore=true,因为它可以防止 Spring Boot 创建一个它可能无法使用的新线程(这适用于 Spring Boot 2.1.0 及更高版本)。多阶段构建A Better Dockerfile中Dockerfile所示的假设假设胖 JAR 已经在命令行上构建。您还可以通过使用多阶段构建并将结果从一个图像复制到另一个图像来在 docker 中执行该步骤。以下示例通过使用 Maven 来实现:DockerfileFROM openjdk:8-jdk-alpine as buildWORKDIR /workspace/appCOPY mvnw .COPY .mvn .mvnCOPY pom.xml .COPY src srcRUN ./mvnw install -DskipTestsRUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)FROM openjdk:8-jdk-alpineVOLUME /tmpARG DEPENDENCY=/workspace/app/target/dependencyCOPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/libCOPY --from=build ${DEPENDENCY}/META-INF /app/META-INFCOPY --from=build ${DEPENDENCY}/BOOT-INF/classes /appENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]复制第一个图像标记为build,它用于运行 Maven、构建胖 JAR 并解压缩它。解包也可以由 Maven 或 Gradle 完成(这是入门指南中采用的方法)。没有太大区别,只是必须编辑构建配置并添加插件。请注意,源代码已分为四层。后面的层包含构建配置和应用程序的源代码,前面的层包含构建系统本身(Maven 包装器)。这是一个小的优化,也意味着我们不必将target目录复制到 docker 镜像,即使是用于构建的临时镜像。RUN每个源代码更改的构建都很慢,因为必须在第一部分重新创建 Maven 缓存。但是你有一个完全独立的构建,只要他们有 docker,任何人都可以运行它来运行你的应用程序。这在某些环境中可能非常有用——例如,您需要与不了解 Java 的人共享您的代码。实验功能Docker 18.06 带有一些“实验性”特性,包括缓存构建依赖项的方法。要打开它们,您需要在守护进程 ( dockerd) 中有一个标志,并在运行客户端时需要一个环境变量。然后你可以添加一个“神奇”的第一行到你的Dockerfile:Dockerfilesyntax=docker/dockerfile:experimental复制然后该RUN指令接受一个新标志:--mount. 以下清单显示了一个完整示例:Dockerfilesyntax=docker/dockerfile:experimentalFROM openjdk:8-jdk-alpine as buildWORKDIR /workspace/appCOPY mvnw .COPY .mvn .mvnCOPY pom.xml .COPY src srcRUN --mount=type=cache,target=/root/.m2 ./mvnw install -DskipTestsRUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)FROM openjdk:8-jdk-alpineVOLUME /tmpARG DEPENDENCY=/workspace/app/target/dependencyCOPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/libCOPY --from=build ${DEPENDENCY}/META-INF /app/META-INFCOPY --from=build ${DEPENDENCY}/BOOT-INF/classes /appENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]复制然后你可以运行它:DOCKER_BUILDKIT=1 docker build -t myorg/myapp .复制以下清单显示了示例输出:... => /bin/sh -c ./mvnw install -DskipTests 5.7s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:3defa... => => naming to docker.io/myorg/myapp复制使用实验性功能,您会在控制台上获得不同的输出,但您可以看到,如果缓存是热的,现在 Maven 构建只需几秒钟而不是几分钟。这个Dockerfile配置的 Gradle 版本非常相似:Dockerfilesyntax=docker/dockerfile:experimentalFROM openjdk:8-jdk-alpine AS buildWORKDIR /workspace/appCOPY . /workspace/appRUN --mount=type=cache,target=/root/.gradle ./gradlew clean buildRUN mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)FROM openjdk:8-jdk-alpineVOLUME /tmpARG DEPENDENCY=/workspace/app/build/dependencyCOPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/libCOPY --from=build ${DEPENDENCY}/META-INF /app/META-INFCOPY --from=build ${DEPENDENCY}/BOOT-INF/classes /appENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]虽然这些功能处于实验阶段,但打开和关闭 buildkit 的选项取决于docker您使用的版本。检查您拥有的版本的文档(前面显示的示例对于docker18.0.6 是正确的)。安全方面就像在经典 VM 部署中一样,进程不应以 root 权限运行。相反,映像应包含运行应用程序的非 root 用户。在 aDockerfile中,您可以通过添加另一个添加(系统)用户和组并将其设置为当前用户(而不是默认的 root)的层来实现此目的:DockerfileFROM openjdk:8-jdk-alpineRUN addgroup -S demo && adduser -S demo -G demoUSER demo...复制如果有人设法突破您的应用程序并在容器内运行系统命令,这种预防措施会限制他们的能力(遵循最小权限原则)。一些进一步的Dockerfile命令只能以 root 身份运行,因此您可能必须将 USER 命令进一步向下移动(例如,如果您计划在容器中安装更多包,它只能以 root 身份运行)。对于其他方法,不使用 aDockerfile可能更适合。例如,在后面描述的 buildpack 方法中,大多数实现默认使用非 root 用户。另一个考虑因素是大多数应用程序在运行时可能不需要完整的 JDK,因此一旦我们进行了多阶段构建,我们就可以安全地切换到 JRE 基础映像。因此,在前面显示的多阶段构建中,我们可以将其用于最终的可运行映像:DockerfileFROM openjdk:8-jre-alpine...复制如前所述,这也节省了映像中的一些空间,这些空间将被运行时不需要的工具占用。文末备注:Spring Boot Docker来源:Spring中国教育管理中心
文章
缓存  ·  安全  ·  Java  ·  Shell  ·  Maven  ·  网络架构  ·  Docker  ·  索引  ·  容器  ·  Spring
2022-06-28
获取异常信息里再出异常就找不到日志了
本系列是 我TM人傻了 系列第三期[捂脸],往期精彩回顾:升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了这个大表走索引字段查询的 SQL 怎么就成全扫描了,我TM人傻了最近组里用第三方给的 SDK 搞了点开发,最近线上突然开始报错,并且发现一个特别奇怪的问题,组员和我说,代码运行到一半不走了,跳过了一段(这代码是刚参加东奥会参加跳远么???)。代码如下,逻辑非常简单:try { log.info("initiate client with conf: {}", conf); SDKClient client = new SDKClient(conf); client.init(); log.info("client initiated"); } catch (Exception e) { log.error("initiate client failed", e); } log.info("start to manipulate...");我们发现 client 实际上没有初始化成功,后面的业务处理一直在报错。查看日志,发现:initiate client with conf: xxxxx start to manipulate...这就是组员说的代码发生了跳跃。因为既没有打印 client initiated,也没有打印 initiate client failed...就直接 start to manipulate... 了。老读者知道,我们的线上是 k8s + Docker,并且每个镜像中内置了 Arthas,并且 Java 版本是 Java 16,并且启用了 JFR。日志中具有链路信息,通过 ELK Agent 拉取到统一日志服务器。这个 SDK 里面要访问的远程地址都有 IP 白名单,我们为了安全本地并不能直接使用 SDK 访问对方的线上环境。在本地测试连接的是对方的测试环境,是没有问题的。所以这里,我们还是得通过 Arthas 进行定位。首先得看看线上运行的源码是否和本地我们看到的一致呢?这个可以通过 jad 命令:jad 要看的类全限定名称查看后发现,反编译后的代码,和我们的源码一致诶。然后我们看看代码的实际执行:trace 要看的类全限定名称 方法之后重新执行这个方法,查看 trace 发现,初始化的时候确实抛出异常了:# 省略我们这里不关心的 +---[min=0.010174ms,max=0.01184ms,total=0.022014ms,count=2] org.apache.logging.log4j.Logger:info() #130 +---[min=599.388978ms,max=630.23967ms,total=1229.628648ms,count=2] com.dasha13.sdk.SDKClient:<init>() #131 +---[min=203.617545ms,max=221.785512ms,total=425.403057ms,count=2] com.dasha13.sdk.SDKClient:init() #132 [throws Exception,2] +---[min=0.034798ms,max=0.084505ms,total=0.119303ms,count=2] org.apache.logging.log4j.Logger:error() #136 +---[min=0.010174ms,max=0.01184ms,total=0.022014ms,count=2] org.apache.logging.log4j.Logger:info() #138但是,这个异常日志,为何没有打印出来呢?我们继续查看下这个异常,使用 watch 方法,并且指定查看深度为 2,这样期望能打印出堆栈以及 Message:watch com.dasha13.sdk.SDKClient init {throwExp} -x 2但是,这里只打印了一个看似是 Message 的信息:method=com.dasha13.sdk.SDKClient init location=AtExceptionExit ts=2021-08-10 02:58:15; [cost=131.20209ms] result=ERROR DATA!!! object class: class java.util.ArrayList, exception class: class com.google.common.util.concurrent.UncheckedExecutionException, exception message: java.lang.IllegalArgumentException这很奇怪,正常来说,指定深度为 2,如果有异常抛出,那么这个输出信息,会包含异常的 Message 以及堆栈信息的。这是怎么回事呢?我们来分别获取堆栈以及信息试试:首先获取堆栈:watch com.dasha13.sdk.SDKClient init {throwExp.getStackTrace()} -x 2重新执行出问题的方法,堆栈正常输出,没啥问题,不过看堆栈应该问题和 Google 的依赖翻转 Bean 管理框架(类似于 Spring) Guice 载入某个 Bean 出异常有关:ts=2021-08-10 03:03:37; [cost=146.644563ms] result=@ArrayList[ @StackTraceElement[][ @StackTraceElement[com.google.inject.internal.InjectorImpl$2.get(InjectorImpl.java:1025)], @StackTraceElement[com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1051)], @StackTraceElement[com.dasha13.sdk.SDKClient.init(SDKClient.java:482)], # 省略之后的再来看异常信息:watch com.dasha13.sdk.SDKClient init {throwExp.getMessage()} -x 2重新执行出问题的方法,这时候发现 watch 失败:watch failed, condition is: null, express is: {throwExp.getMessage()}, com.google.common.util.concurrent.UncheckedExecutionException: java.lang.IllegalArgumentException, visit /app/arthas/arthas.log for more details.我们按照提示,查看 arthas 日志,发现的异常堆栈:2021-08-10 03:07:11 [XNIO-2 task-3] ERROR c.t.a.c.command.express.OgnlExpress -Error during evaluating the expression: com.google.common.util.concurrent.UncheckedExecutionException: java.lang.IllegalArgumentException at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2203) at com.google.common.cache.LocalCache.get(LocalCache.java:3937) at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3941) at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4824) at com.google.common.cache.LocalCache$LocalLoadingCache.getUnchecked(LocalCache.java:4830) at com.google.inject.internal.util.StackTraceElements.forMember(StackTraceElements.java:66) at com.google.inject.internal.Errors.formatSource(Errors.java:806) at com.google.inject.internal.Errors.formatSource(Errors.java:785) at com.google.inject.internal.Errors.formatInjectionPoint(Errors.java:839) at com.google.inject.internal.Errors.formatSource(Errors.java:800) at com.google.inject.internal.Errors.formatSource(Errors.java:785) at com.google.inject.internal.Errors.format(Errors.java:584) at com.google.inject.ProvisionException.getMessage(ProvisionException.java:60) cause by: MethodNotFoundException: Method not found: class com.google.common.xxxxxxxxx我们发现,居然是 ProvisionException 的 getMessage() 发生了异常,也就是异常的 getMessage() 发生了异常.查看异常的 Cause 我们也定位出来,是 Guava 版本与 guice 版本不兼容导致,其根本原因是三方接口超时,导致初始化异常,有异常抛出被封装成 ProvisionException, ProvisionException 异常的 getMessage 依赖 Guava Cache 缓存一些异常信息,但是我们项目中 Guava 版本与 guice 版本不兼容,导致某些方法不存在,所以 ProvisionException 异常的 getMessage 也会有异常。之前运行没问题是因为三方没有还没有过初始化的时候接口超时抛异常。。。我们使用的 log4j2 异步日志配置,并且将异常作为最后一个参数传入日志方法中,正常情况下,会输出这个异常的 Message 以及异常堆栈.但从上面的分析我们知道,获取 Message 的时候,抛出了异常。Log4j 的设计是使用了日志事件的生产消费这种架构。这里是消费者获取异常的 Message 以及异常堆栈,并且在获取 Message 的时候,发现有异常。对于 Log4j2 异步日志,发现有异常的时候,原有日志事件会被直接抛弃,并将异常输出到 StatusLogger 中(底层其实就是标准异常输出)中,这里对应 log4j 的源码:AppenderControlprivate void tryCallAppender(final LogEvent event) { try { //调用 appender 输出日志 appender.append(event); } catch (final RuntimeException error) { //处理 RuntimeException handleAppenderError(event, error); } catch (final Exception error) { //处理其他 Exception handleAppenderError(event, new AppenderLoggingException(error)); } } private void handleAppenderError(final LogEvent event, final RuntimeException ex) { appender.getHandler().error(createErrorMsg("An exception occurred processing Appender "), event, ex); if (!appender.ignoreExceptions()) { throw ex; } }ErrorHandler 一般都是默认实现,即 DefaultErrorHandler;DefaultErrorHandler 是输出到一个 StatusLogger:DefaultErrorHandlerprivate static final Logger LOGGER = StatusLogger.getLogger(); public void error(final String msg, final LogEvent event, final Throwable t) { final long current = System.nanoTime(); if (current - lastException > EXCEPTION_INTERVAL || exceptionCount++ < MAX_EXCEPTIONS) { LOGGER.error(msg, t); } lastException = current; if (!appender.ignoreExceptions() && t != null && !(t instanceof AppenderLoggingException)) { throw new AppenderLoggingException(msg, t); } }StatusLogger 其实就是标准异常输出 System.err:StatusLoggerthis.logger = new SimpleLogger("StatusLogger", Level.ERROR, false, true, showDateTime, false, dateFormat, messageFactory, PROPS, //标准异常输出 System.err);我们部署架构中,将标准异常输出放到了一个很偏僻的位置,基本没有人看,所以没注意到。。。查看标准异常输出,会发现的确有异常:2021-08-10 03:30:29,810 Log4j2-TF-10-AsyncLoggerConfig-3 ERROR An exception occurred processing Appender file com.google.common.util.concurrent.UncheckedExecutionException: java.lang.IllegalArgumentException at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2203) at com.google.common.cache.LocalCache.get(LocalCache.java:3937) at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3941) at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4824) at com.google.common.cache.LocalCache$LocalLoadingCache.getUnchecked(LocalCache.java:4830) at com.google.inject.internal.util.StackTraceElements.forMember(StackTraceElements.java:66) at com.google.inject.internal.Errors.formatSource(Errors.java:806) at com.google.inject.internal.Errors.formatSource(Errors.java:785) at com.google.inject.internal.Errors.formatInjectionPoint(Errors.java:839) at com.google.inject.internal.Errors.formatSource(Errors.java:800) at com.google.inject.internal.Errors.formatSource(Errors.java:785) at com.google.inject.internal.Errors.format(Errors.java:584) at com.google.inject.ProvisionException.getMessage(ProvisionException.java:60) at org.apache.logging.log4j.core.impl.ThrowableProxy.<init>(ThrowableProxy.java:105) at org.apache.logging.log4j.core.impl.ThrowableProxy.<init>(ThrowableProxy.java:93) at org.apache.logging.log4j.core.impl.Log4jLogEvent.getThrownProxy(Log4jLogEvent.java:629) at org.apache.logging.log4j.core.pattern.ExtendedThrowablePatternConverter.format(ExtendedThrowablePatternConverter.java:63) at org.springframework.boot.logging.log4j2.ExtendedWhitespaceThrowablePatternConverter.format(ExtendedWhitespaceThrowablePatternConverter.java:50) at org.apache.logging.log4j.core.pattern.PatternFormatter.format(PatternFormatter.java:38) at org.apache.logging.log4j.core.layout.PatternLayout$PatternSerializer.toSerializable(PatternLayout.java:345) at org.apache.logging.log4j.core.layout.PatternLayout.toText(PatternLayout.java:244) at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:229) at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:59) at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:197) at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:190) at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:181) at org.apache.logging.log4j.core.appender.RollingFileAppender.append(RollingFileAppender.java:312) at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156) at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129) at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120) at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84) at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:543) at org.apache.logging.log4j.core.async.AsyncLoggerConfig.callAppenders(AsyncLoggerConfig.java:127) at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:502) at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:485) at org.apache.logging.log4j.core.async.AsyncLoggerConfig.log(AsyncLoggerConfig.java:121) at org.apache.logging.log4j.core.async.AsyncLoggerConfig.logToAsyncLoggerConfigsOnCurrentThread(AsyncLoggerConfig.java:169) at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor$Log4jEventWrapperHandler.onEvent(AsyncLoggerConfigDisruptor.java:111) at org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor$Log4jEventWrapperHandler.onEvent(AsyncLoggerConfigDisruptor.java:97) at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:168) at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125) at java.base/java.lang.Thread.run(Thread.java:834) Caused by: java.lang.IllegalArgumentException at com.google.inject.internal.asm.$ClassReader.<init>(Unknown Source) at com.google.inject.internal.asm.$ClassReader.<init>(Unknown Source) at com.google.inject.internal.asm.$ClassReader.<init>(Unknown Source) at com.google.inject.internal.util.LineNumbers.<init>(LineNumbers.java:65) at com.google.inject.internal.util.StackTraceElements$1.load(StackTraceElements.java:46) at com.google.inject.internal.util.StackTraceElements$1.load(StackTraceElements.java:43) at com.google.common.cache.LocalCache$LoadingValueReference.loadFuture(LocalCache.java:3527) at com.google.common.cache.LocalCache$Segment.loadSync(LocalCache.java:2319) at com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2282) at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2197) ... 41 more并且,在这之后,会根据 Appender 的 ignoreExceptions 配置(默认都是 true),决定调用日志方法的地方是否会抛出异常,但这个是针对同步日志的,异步日志即将异常抛到 Disruptor 的异常处理器,Log4j2 Disruptor 的异常处理也是将异常输出到 System.err 也就是标准异常输出。默认情况下是不抛出的,毕竟对于同步日志没人希望因为日志有异常就让业务不能正常进行,异步日志由于前面的处理已经输出到标准异常输出这里就没必要多此一举了。
文章
Arthas  ·  SQL  ·  缓存  ·  Kubernetes  ·  Java  ·  测试技术  ·  开发工具  ·  Docker  ·  Spring  ·  容器
2022-06-25
升级到Spring 5.3.x之后,GC次数急剧增加
最近我们项目升级到了 Spring Boot 2.4.6 + Spring Cloud 2020.0.x,通过我的另一系列即可看出:Spring Cloud 升级之路。但是升级后,我们发现 YoungGC 明显增高,分配对象速率明显增高,但是晋升的对象并没有增多,证明都是新创建的对象并且没过多久就可以被回收。我们来看其中一个进程的监控,这时候的 http 请求速率大概在 100 左右:这就很奇怪了,请求速率并没有那么大,但是通过监控可以看出每秒钟分配了将近两个 G 的内存。在升级之前,这个分配速率大概在 100~200 MB 左右,在同等请求速率下。那么这多出来的内存究竟是哪里消耗的呢?我们需要看一下内存中各种对象的统计数据,即使用 jmap 命令。同时不能只查看存活对象的统计,因为从监控中看出来并不是老年代对象过多,因为晋升的对象并没有增多,相反的,我们如果我们能排除现在还存活的对象就更好了。同时,由于 GC 相当频繁,1s 左右就会有一次。所以基本不能期望一次就能抓到我们想要的 jmap。同时 jmap 会导致所有线程进入 safepoint 从而 STW,对线上有一定影响,所以不能太频繁 jmap。所以,我们采取如下策略:扩容一个实例,之后将一个实例,通过注册中心以及限流器将某个实例的流量切走一半;针对这个实例,连续执行 jmap -histo(统计所有对象) 以及 jmap -histo:live(仅统计存活对象);重复第二步 5 次,每次间隔 100ms,300ms,500ms,700ms;去掉限流这个实例的限流,将新扩容的实例关闭。通过这几次的 jmap 对比,我们发现 jmap 统计中排在前面的对象类型有一个 spring 框架的:num #instances #bytes class name (module) ------------------------------------------------------- 1: 7993252 601860528 [B (java.base@11.0.8) 2: 360025 296261160 [C (java.base@11.0.8) 3: 10338806 246557984 [Ljava.lang.Object; (java.base@11.0.8) 4: 6314471 151547304 java.lang.String (java.base@11.0.8) 5: 48170 135607088 [J (java.base@11.0.8) 6: 314420 126487344 [I (java.base@11.0.8) 7: 4591109 110100264 [Ljava.lang.Class; (java.base@11.0.8) 8: 245542 55001408 org.springframework.core.ResolvableType 9: 205234 29042280 [Ljava.util.HashMap$Node; (java.base@11.0.8) 10: 386252 24720128 [org.springframework.core.ResolvableType; 11: 699929 22397728 java.sql.Timestamp (java.sql@11.0.8) 12: 89150 21281256 [Ljava.beans.PropertyDescriptor; (java.desktop@11.0.8) 13: 519029 16608928 java.util.HashMap$Node (java.base@11.0.8) 14: 598728 14369472 java.util.ArrayList (java.base@11.0.8)这个对象是怎么创建出来的呢?如何定位一个已经不再存活的频繁创建对象,并且这个对象类型是框架内部的?首先,MAT(Eclipse Memory Analyzer)+ jmap dump 这种整个堆分析,并不太适用,原因是:对象已经不再存活,MAT 更适合对于内存泄漏的分析,我们这里是创建出来很多预期外的对象,占用了大量内存,这些对象很快就不再存活。MAT 对于不再存活的对象,无法准确分析出创建者,主要因为 dump 的时候不确定是否能抓到我们想要的信息,或者有很多信息噪声。虽然这个问题不能这么定位,我还是将我采集的 jmap dump 结果放在这里用 MAT 分析的结果展示出来给大家看下:那么接下来怎么分析呢?这就又用到了我们的老朋友,JFR + JMC。老读者知道,我经常使用 JFR 定位线上问题,这里怎么使用呢?并没有直接的 JFR 事件统计经常创建哪些对象,但是呢,有间接的事件,可以间接体现是谁创建了这么多对象。我一般这么定位:通过线程分配对象统计事件查看是哪个线程分配对象过多(Thread Allocation Statistics)。通过热点代码分析哪些热点代码可能会产生这些对象(Method Profiling Sample)。像这种大量创建的对象,抓取 Runnable 代码很大概率被抓取到,并且在事件中占比高。首先查看 Thread Allocation Statistics 事件,发现基本上所有 servlet 线程(就是处理 Http 请求的线程,我们用的 Undertow,所以线程名称是 XNIO 开头的),分配的对象都很多,这样并不能定位问题:然后我们来看热点代码统计,点击 Method Profiling Sample 事件,查看堆栈追踪统计,看哪些占比比较高。发现占比靠前的,貌似都和这个 ResolvableType 有关,进一步定位,双击第一个方法查看调用堆栈统计:我们发现,调用它的是 BeanUtils.copyProperties。查看其它ResolvableType 有关的调用,都和BeanUtils.copyProperties有关。这个方法是我们项目中经常使用的方法,用于同类型或者不同类型之间的属性复制。这个方法为何会创建这么多 ResolvableType 呢?通过查看源码,我们发现从 Spring 5.3.x 开始,BeanUtils 开始通过创建 ResolvableType 这个统一类信息封装,进行属性复制:/** * * <p>As of Spring Framework 5.3, this method honors generic type information */ private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException { }里面的源码,每次都针对源对象和目标对象的类型的每个属性方法创建了新的 ResolvableType,并且没有做缓存。这导致一次复制,会创建出来大量的 ResolvableType.我们来做个试验:public class Test { public static void main(String[] args) { TestBean testBean1 = new TestBean("1", "2", "3", "4", "5", "6", "7", "8", "1", "2", "3", "4", "5", "6", "7", "8"); TestBean testBean2 = new TestBean(); for (int i = 0; i > -1; i++) { BeanUtils.copyProperties(testBean1, testBean2); System.out.println(i); } } }分别使用 spring-beans 5.2.16.RELEASE 和 spring-beans 5.3.9 这两个依赖去执行这个代码,JVM 参数使用 -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx512m.这些参数的意思是,使用 EpsilonGC,也就是在堆内存满的时候,不执行 GC,直接抛出 OutofMemory 异常并程序结束,并且最大堆内存是 512m。这样,程序其实就是看:在内存耗尽之前,不同版本的 BeanUtils.copyProperties 分别能执行多少次。试验结果是:spring-beans 5.2.16.RELEASE 是 444489 次,spring-beans 5.3.9 是 27456 次。这是相当大的差距啊。于是,针对这个问题,我向 spring-framework github 提了个 Issue.然后,对于项目中经常使用 BeanUtils.copyProperties 的地方,替换成使用 BeanCopier,并且封装了一个简单类:public class BeanUtils { private static final Cache<String, BeanCopier> CACHE = Caffeine.newBuilder().build(); public static void copyProperties(Object source, Object target) { Class<?> sourceClass = source.getClass(); Class<?> targetClass = target.getClass(); BeanCopier beanCopier = CACHE.get(sourceClass.getName() + " to " + targetClass.getName(), k -> { return BeanCopier.create(sourceClass, targetClass, false); }); beanCopier.copy(source, target, null); } }但是需要注意的是,BeanCopier替换BeanUtils.copyProperties最直接的一个问题就是:对于属性不同但是名字相同的无法复制。例如一个是 int 另一个是 Integer 也不行。同时还有深拷贝的一些区别,需要我们做好单元测试。修改好后,问题解决。
文章
缓存  ·  监控  ·  Java  ·  测试技术  ·  Android开发  ·  Spring
2022-06-25
启用 Spring-Cloud-OpenFeign 配置可刷新,项目无法启动(下)
本篇文章涉及底层设计以及原理,以及问题定位,比较深入,篇幅较长,所以拆分成上下两篇:上:问题简单描述以及 Spring Cloud RefreshScope 的原理下:当前 spring-cloud-openfeign + spring-cloud-sleuth 带来的 bug 以及如何修复Spring Cloud 中的配置动态刷新其实在测试的程序中,我们已经实现了一个简单的 Bean 刷新的设计。Spring Cloud 的自动刷新中,包含两种元素的刷新,分别是:配置刷新,即 Environment.getProperties 和 @ConfigurationProperties 相关 Bean 的刷新添加了 @RefreshScope 注解的 Bean 的刷新@RefreshScope 注解其实和我们上面自定义 Scope 使用的注解配置类似,即指定名称为 refresh,同时使用 CGLIB 代理:RefreshScope@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Scope("refresh") @Documented public @interface RefreshScope { ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; }同时需要自定义 Scope 进行注册,这个自定义的 Scope 即 org.springframework.cloud.context.scope.refresh.RefreshScope,他继承了 GenericScope,我们先来看这个父类,我们专注我们前面测试的那三个 Scope 接口方法,首先是 get:private BeanLifecycleWrapperCache cache = new BeanLifecycleWrapperCache(new StandardScopeCache()); @Override public Object get(String name, ObjectFactory<?> objectFactory) { //放入缓存 BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory)); this.locks.putIfAbsent(name, new ReentrantReadWriteLock()); try { //这里在第一次调用会创建 Bean 实例,所以需要上锁,保证只创建一次 return value.getBean(); } catch (RuntimeException e) { this.errors.put(name, e); throw e; } }然后是注册 Destroy 的回调,其实就放在对应的 Bean 中,在移除的时候,会调用这个回调:@Override public void registerDestructionCallback(String name, Runnable callback) { BeanLifecycleWrapper value = this.cache.get(name); if (value == null) { return; } value.setDestroyCallback(callback); }最后是移除 Bean,就更简单了,从缓存中移除这个 Bean:@Override public Object remove(String name) { BeanLifecycleWrapper value = this.cache.remove(name); if (value == null) { return null; } return value.getBean(); }这样,如果缓存中的 bean 被移除,下次调用 get 的时候,就会重新生成 Bean。并且,由于 RefreshScope 注解中默认的 ScopedProxyMode 为 CGLIB 代理模式,所以每次通过 BeanFactory 获取 Bean 以及自动装载的 Bean 调用的时候,都会调用这里 Scope 的 get 方法。Spring Cloud 将动态刷新接口通过 Spring Boot Actuator 进行暴露,对应路径是 /actuator/refresh,对应源码是:RefreshEndpoint@Endpoint(id = "refresh") public class RefreshEndpoint { private ContextRefresher contextRefresher; public RefreshEndpoint(ContextRefresher contextRefresher) { this.contextRefresher = contextRefresher; } @WriteOperation public Collection<String> refresh() { Set<String> keys = this.contextRefresher.refresh(); return keys; } }可以看出其核心是 ContextRefresher,他的核心逻辑也非常简单:ContextRefresherpublic synchronized Set<String> refresh() { Set<String> keys = refreshEnvironment(); //刷新 RefreshScope this.scope.refreshAll(); return keys; } public synchronized Set<String> refreshEnvironment() { //提取 SYSTEM、JNDI、SERVLET 之外所有参数变量 Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources()); //从配置源更新 Environment 中的所有属性 updateEnvironment(); //与刷新前作对比,提取出所有变了的属性 Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet(); //将该变了的属性,放入 EnvironmentChangeEvent 并发布 this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys)); //返回所有改变的属性 return keys; }调用 RefreshScope 的 RefreshAll,其实就是调用我们上面说的 GenericScope 的 destroy,之后发布 RefreshScopeRefreshedEvent:public void refreshAll() { super.destroy(); this.context.publishEvent(new RefreshScopeRefreshedEvent()); }GenericScope 的 destroy 其实就是将缓存清空,这样所有标注 @RefreshScope 注解的 Bean 都会被重建。问题定位通过上篇的源码分析,我们知道,如果想实现 Feign.Options 的动态刷新,目前我们不能把它放入 NamedContextFactory 生成的 ApplicationContext 中,而是需要将它放入项目的根 ApplicationContext 中,这样 Spring Cloud 暴露的 refresh actuator 接口,才能正确刷新。spring-cloud-openfeign 中,也是这么实现的。如果配置了feign.client.refresh-enabled: true那么在初始化每个 FeignClient 的时候,就会将 Feign.Options 这个 Bean 注册到根 ApplicationContext,对应源码:FeignClientsRegistrarprivate void registerOptionsBeanDefinition(BeanDefinitionRegistry registry, String contextId) { if (isClientRefreshEnabled()) { //使用 "feign.Request.Options-FeignClient 的 contextId" 作为 Bean 名称 String beanName = Request.Options.class.getCanonicalName() + "-" + contextId; BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder .genericBeanDefinition(OptionsFactoryBean.class); //设置为 RefreshScope definitionBuilder.setScope("refresh"); definitionBuilder.addPropertyValue("contextId", contextId); BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(), beanName); //注册为 CGLIB 代理的 Bean definitionHolder = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true); //注册 Bean BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry); } } private boolean isClientRefreshEnabled() { return environment.getProperty("feign.client.refresh-enabled", Boolean.class, false); }这样,在调用 /actuator/refresh 接口的时候,这些 Feign.Options 也会被刷新。但是注册到根 ApplicationContext 中的话,对应的 FeignClient 如何获取这个 Bean 使用呢?即在 Feign 的 NamedContextFactory (即 FeignContext )中生成的 ApplicationContext 中,如何找到这个 Bean 呢?这个我们不用担心,因为所有的 NamedContextFactory 生成的 ApplicationContext 的 parent,都设置为了根 ApplicationContext,参考源码:public abstract class NamedContextFactory<C extends NamedContextFactory.Specification> implements DisposableBean, ApplicationContextAware { private ApplicationContext parent; @Override public void setApplicationContext(ApplicationContext parent) throws BeansException { this.parent = parent; } protected AnnotationConfigApplicationContext createContext(String name) { //省略其他代码 if (this.parent != null) { // Uses Environment from parent as well as beans context.setParent(this.parent); } //省略其他代码 } }这样设置后,FeignClient 在自己的 ApplicationContext 中如果找不到的话,就会去 parent 的 ApplicationContext 也就是根 ApplicationContext 去找。这样看来,设计是没问题的,但是我们的项目启动不了,应该是启用其他依赖导致的。我们在获取 Feign.Options Bean 的地方打断点调试,发现并不是直接从 FeignContext 中获取 Bean,而是从 spring-cloud-sleuth 的 TraceFeignContext 中获取的。spring-cloud-sleuth 为了保持链路,在很多地方增加了埋点,对于 OpenFeign 也不例外。在 FeignContextBeanPostProcessor,将 FeignContext 包装了一层变成了 TraceFeignContext:public class FeignContextBeanPostProcessor implements BeanPostProcessor { private final BeanFactory beanFactory; public FeignContextBeanPostProcessor(BeanFactory beanFactory) { this.beanFactory = beanFactory; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof FeignContext && !(bean instanceof TraceFeignContext)) { return new TraceFeignContext(traceFeignObjectWrapper(), (FeignContext) bean); } return bean; } private TraceFeignObjectWrapper traceFeignObjectWrapper() { return new TraceFeignObjectWrapper(this.beanFactory); } }这样,FeignClient 会从这个 TraceFeignContext 中读取 Bean,而不是 FeignContext。但是通过源码我们发现,TraceFeignContext 并没有设置 parent 为根 ApplicationContext,所以找不到注册到根 ApplicationContext 中的 Feign.Options 这些 Bean。解决问题针对这个 Bug,我向 spring-cloud-sleuth 和 spring-cloud-commons 分别提了修改:add getter for parent in NamedContextFactoryfix #2023, add parent in the new TraceFeignContext大家如果在项目中使用了 spring-cloud-sleuth,对于 spring-cloud-openfeign 想开启自动刷新的话,可以考虑使用同名同路径的类替换代码先解决这个问题。等待我提交的代码发布新版本了。参考代码:public class FeignContextBeanPostProcessor implements BeanPostProcessor { private static final Field PARENT; private static final Log logger = LogFactory.getLog(FeignContextBeanPostProcessor.class); static { try { PARENT = NamedContextFactory.class.getDeclaredField("parent"); PARENT.setAccessible(true); } catch (Exception e) { throw new Error(e); } } private final BeanFactory beanFactory; public FeignContextBeanPostProcessor(BeanFactory beanFactory) { this.beanFactory = beanFactory; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof FeignContext && !(bean instanceof TraceFeignContext)) { FeignContext feignContext = (FeignContext) bean; TraceFeignContext traceFeignContext = new TraceFeignContext(traceFeignObjectWrapper(), feignContext); try { traceFeignContext.setApplicationContext((ApplicationContext) PARENT.get(bean)); } catch (IllegalAccessException e) { logger.warn("Cannot find parent in FeignContext: " + beanName); } return traceFeignContext; } return bean; } private TraceFeignObjectWrapper traceFeignObjectWrapper() { return new TraceFeignObjectWrapper(this.beanFactory); } }
文章
缓存  ·  Java  ·  Spring
2022-06-25
1 2 3 4 5 6 7 8 9
...
20
跳转至:
微服务
22835 人关注 | 9967 讨论 | 22144 内容
+ 订阅
  • 阿里云ECS使用体验
  • 你的 Sleep 服务会梦到服务网格外的 bookinfo 吗
  • IOC-golang 的 AOP 原理与应用
查看更多 >
开发与运维
5252 人关注 | 125986 讨论 | 203908 内容
+ 订阅
  • PolarDB-X适用场景
  • 我的ECS使用体验报告
  • esc使用体验心得
查看更多 >
云原生
230336 人关注 | 9776 讨论 | 29993 内容
+ 订阅
  • PolarDB-X适用场景
  • PolarDB-X
  • 极简,利用Docker仅两行命令就能下载和编译OpenJDK11
查看更多 >
数据库
249374 人关注 | 44651 讨论 | 63083 内容
+ 订阅
  • PolarDB-X适用场景
  • PolarDB-X高兼容
  • PolarDB-X
查看更多 >
安全
1063 人关注 | 23293 讨论 | 56594 内容
+ 订阅
  • esc使用体验心得
  • 服务器使用
  • 阿里云ECS服务器使用体验
查看更多 >