软件工程的实践

发布于:2025-06-14 ⋅ 阅读:(19) ⋅ 点赞:(0)

J2EE规范包含Servlet、JDBC等技术规范。

J2EE平台由一整套服务、应用程序接口、和协议构成,主要有十三个规范,分别是Servlet、XML、JMS、JTA、JTS、JavaMail、JAF、JDBC、JNDI、EJB、RMI、CORBA、JSP。其中,Servlet是一种小型的Java程序,它扩展了Web服务器的功能。作为一种服务器端的引用,当被请求时开始执行。JDBC(Java Database Connectivity)为Java开发人员提供了一个行业标准API,可以在Java应用于关系数据库之间建立起独立于数据库的连接。

J2EE实际上是一个插件系统,只要继承Servlet的接口,就可以部署服务。

如需更多关于“J2EE规范包含servlet jdbc”的信息,建议咨询专业编程技术人员。

一流企业制定标准的含义,不是说写一个文档,而是构建一个接口体系。供应链整体都围绕这个接口来实现。实现整体社会化分工的效益。

一流企业定标准是指这些企业在其行业中占据领导地位,并制定和推广行业标准。这些企业通常具有强大的技术实力和创新能力,能够为整个行业的发展做出重要贡献。

首先,一流企业定标准能够推动行业技术的发展。这些企业通过制定标准,将自身掌握的核心技术、专利等优势转化为行业标准,从而推动整个行业的技术进步。这不仅能够提升企业的竞争力和市场份额,还能够促进行业的可持续发展。

其次,一流企业定标准能够提高产品质量和可靠性。通过制定和执行标准,企业能够确保产品的质量和可靠性,从而赢得消费者的信任和忠诚度。这有助于企业在市场竞争中树立良好的形象,提高品牌价值和市场份额。

此外,一流企业定标准还能够促进产业升级和优化。随着技术的不断发展和市场需求的变化,企业需要不断更新和升级自身的产品和技术。通过制定和执行标准,企业能够加速产业升级和优化的进程,从而更好地满足市场需求和提高竞争力。

总之,一流企业定标准是其发展的重要战略之一,这些企业通过制定和推广行业标准,提升了整个行业的竞争力和可持续发展能力。同时,这也要求企业不断加强自身的技术研发和创新实力,以保持其在行业中的领先地位。

软件工程的实践-余春龙

方法论

及时出错

版本控制,单元测试,持续集成,持续部署

架构人员,软件技能要精湛。并且业务上要深入理解才有可能称为优秀的架构人员。

用户需求收集的方法

实例化工程,战斗机的选择:增加一个内容

7个步骤

角色分析

边界分析

。。。参考焦雨涵的文档

大概3-5页

架构始于建筑,是因为人类发展(原始人自给自足住在树上,也就不需要架构),分工协作的需要,将目标系统按某个原则进行切分,切分的原则,是要便于不同的角色进行并行工作。

根据我们关注的角度不同,可以将架构分成三种:

1.逻辑架构

软件系统中元件之间的关系,比如用户界面,数据库,外部系统接口,商业逻辑元件,等等。

从上面这张图中可以看出,此系统被划分成三个逻辑层次,即表象层次,商业层次和数据持久层次。每一个层次都含有多个逻辑元件。比如WEB服务器层次中有HTML服务元件、Session服务元件、安全服务元件、系统管理元件等。

2.物理架构

软件元件是怎样放到硬件上的。

比如下面这张物理架构图,图中所有的元件都是物理设备,包括网络分流器、代理服务器、WEB服务器、应用服务器、报表服务器、整合服务器、存储服务器、主机等等。

3.系统架构

系统的非功能性特征,如可扩展性、可靠性、强壮性、灵活性、性能等。

系统架构的设计要求架构师具备软件和硬件的功能和性能的过硬知识,这一工作无疑是架构设计工作中最为困难的工作。

此外,从每一个角度上看,都可以看到架构的两要素:元件划分和设计决定。

首先,一个软件系统中的元件首先是逻辑元件。这些逻辑元件如何放到硬件上,以及这些元件如何为整个系统的可扩展性、可靠性、强壮性、灵活性、性能等做出贡献,是非常重要的信息。

其次,进行软件设计需要做出的决定中,必然会包括逻辑结构、物理结构,以及它们如何影响到系统的所有非功能性特征。这些决定中会有很多是一旦作出,就很难更改的。

根据作者的经验,一个基于数据库的系统架构,有多少个数据表,就会有多少页的架构设计文档。比如一个中等的数据库应用系统通常含有一百个左右的数据表,这样的一个系统设计通常需要有一百页左右的架构设计文档。

为什么需要架构?

有系统的地方就需要架构,大到航空飞机,小到一个电商系统里面的一个功能组件都需要设计和架构。

-----------------------------------

©著作权归作者所有:来自51CTO博客作者禅与计算机程序设计艺术的原创作品,请联系作者获取转载授权,否则将追究法律责任

软件架构设计杂记: 分层架构 与 PO、VO、DTO、BO、POJO、BO/DO、DAO

QO(Query):查询对象,它主要用于定义查询条件和规则,用于接收前端传递的查询条件参数。

https://blog.51cto.com/u_15236724/5766841

软件架构

软件架构是一个系统的草图。软件架构描述的对象是直接构成系统的抽象组件。各个组件之间的连接则明确和相对细致地描述组件之间的通信。在实现阶段,这些抽象组件被细化为实际的组件,比如具体某个类或者对象。在面向对象领域中,组件之间的连接通常用接口来实现。

软件架构为软件系统提供了一个结构、行为和属性的高级抽象,由构件的描述、构件的相互作用、指导构件集成的模式以及这些模式的约束组成。软件架构不仅显示了软件需求和软件结构之间的对应关系,而且指定了整个软件系统的组织和拓扑结构,提供了一些设计决策的基本原理。

软件架构的核心价值应该只围绕一个核心命题:控制复杂性。他并不意味着某个特定的分层结构,某个特定的方法论(贫血、DDD等)。

-----------------------------------

©著作权归作者所有:来自51CTO博客作者禅与计算机程序设计艺术的原创作品,请联系作者获取转载授权,否则将追究法律责任

软件架构设计杂记: 分层架构 与 PO、VO、DTO、BO、POJO、BO/DO、DAO

https://blog.51cto.com/u_15236724/5766841

业务架构:由业务架构师负责,也可以称为业务领域专家、行业专家。业务架构属于顶层设计,其对业务的定义和划分会影响组织结构和技术架构。例如,阿里巴巴在没有中台部门之前,每个业务部门的技术架构都是烟囱式的,淘宝、天猫、飞猪、1688等各有一套体系结构。而后,成立了共享平台事业部,打通了账号、商品、订单等体系,让商业基础实施的复用成为可能。

应用架构:由应用架构师负责,他需要根据业务场景的需要,设计应用的层次结构,制定应用规范、定义接口和数据交互协议等。并尽量将应用的复杂度控制在一个可以接受的水平,从而在快速的支撑业务发展的同时,在保证系统的可用性和可维护性的同时,确保应用满足非功能属性要求(性能、安全、稳定性等)。

分布式系统架构:分布式系统基本是稍具规模业务的必选项。它需要解决服务器负载,分布式服务的注册和发现,消息系统,缓存系统,分布式数据库等问题,同时架构师要在CAP(Consistency,Availability,Partition tolerance)之间进行权衡。

数据架构:对于规模大一些的公司,数据治理是一个很重要的课题。如何对数据收集、数据处理提供统一的服务和标准,是数据架构需要关注的问题。其目的就是统一数据定义规范,标准化数据表达,形成有效易维护的数据资产,搭建统一的大数据处理平台,形成数据使用闭环。

物理架构:物理架构关注软件元件是如何放到硬件上的,包括机房搭建、网络拓扑结构,网络分流器、代理服务器、Web服务器、应用服务器、报表服务器、整合服务器、存储服务器和主机等。

运维架构:负责运维系统的规划、选型、部署上线,建立规范化的运维体系。

-----------------------------------

©著作权归作者所有:来自51CTO博客作者禅与计算机程序设计艺术的原创作品,请联系作者获取转载授权,否则将追究法律责任

软件架构设计杂记: 分层架构 与 PO、VO、DTO、BO、POJO、BO/DO、DAO

https://blog.51cto.com/u_15236724/5766841

DDD-领域建模

TDD

业务架构

需求分析

永远紧盯目标,将相关干系人的期望列出来,然后进行处理调整。后期的各个阶段,永远对应这个内容。

设计模式

变化点的列出

贫血模式

充血模式

根据业务架构实践,结合业界分层规范与流行技术框架分析,推荐分层结构如图所示,默认上层 依赖于下层,箭头关系表示可直接依赖,如:开放 API 层可以依赖于 Web 层(Controller 层),也可以 直接依赖于 Service 层,依此类推:

开放 API 层:可直接封装 Service 接口暴露成 RPC 接口;通过 Web 封装成 http 接口;网关控制层等。

终端显示层:各个端的模板渲染并执行显示的层。当前主要是 freemaker、velocity 渲染,JS 渲染,JSP 渲染,移动端展示等。

Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。

Service 层:相对具体的业务逻辑服务层。

Manager 层:通用业务处理层,它有如下特征

1)对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口。

2)对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。

3)与 DAO 层交互,对多个 DAO 的组合复用。

DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase、OceanBase等进行数据交互。

第三方服务:包括其它部门 RPC 服务接口,基础平台,其它公司的 HTTP 接口,如淘宝开放平台、支付宝付款服务、 高德地图服务等。

外部数据接口:外部(应用)数据存储服务提供的接口,多见于数据迁移场景中。

3.【参考】分层领域模型规约:

DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。

​有时候称为PO persistent object​​,持久对象。与数据库里表字段一一对应。PO是一些属性,以及set和get方法组成。一般情况下,一个表对应一个PO,直接与操作数据库的crud相关。

DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。

​​data trasfer object​​,数据传输对象。主要用于远程调用等需要大量传输对象的地方。

比如我们一张表有 100 个字段,那么对应的 PO 就有 100 个属性。 但是我们界面上只要显示 10 个字段, 客户端用 WEB service 来获取数据,没有必要把整个 PO 对象传递到客户端,

这时我们就可以用只有这 10 个属性的 DTO 来传递结果到客户端,这样也不会暴露服务端表结构 . 到达客户端以后,如果用这个对象来对应界面显示,那此时它的身份就转为 VO。

-----------------------------------

©著作权归作者所有:来自51CTO博客作者禅与计算机程序设计艺术的原创作品,请联系作者获取转载授权,否则将追究法律责任

软件架构设计杂记: 分层架构 与 PO、VO、DTO、BO、POJO、BO/DO、DAO

https://blog.51cto.com/u_15236724/5766841

BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。

​​bussiness object​​​业务对象、​​Domain Object​​域对象。封装业务逻辑的 Java 对象 , 通过调用 DAO 方法 , 结合 PO,VO 进行业务操作。 一个BO对象可以包括多个PO对象。如常见的工作简历例子为例,简历可以理解为一个BO,简历又包括工作经历,学习经历等,这些可以理解为一个个的PO,由多个PO组成BO。

Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。

VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。

​view object​​​/​​value object​​,表现层对象。通常用于业务层之间的数据传递,和PO一样也是仅仅包含数据而已。但应是抽象出的业务对象,可以和表对应,也可以不。这根据业务的需要而定。对于页面上要展示的对象,可以封装一个VO对象,将所需数据封装进去。

Dao

​data access object​​,数据访问对象。此对象用于访问数据库。通常和 PO 结合使用, DAO 中包含了各种数据库的操作方法。通过它的方法 , 结合 PO 对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。

数据架构演进

基础架构中服务端就一台主机,其中存储了应用程序和数据库,刚上线时是没有问题的,

当数据和流量变得越来越大的时候就难以应付了,这时候就需要将应用程序和数据库分别放到不同的主机巾

1.4 数据层面的解决方案

现在无论是企业的业务系统还是互联网上的网站程序都面临着数据量大的问题,这个问题

如果解决不好将严重影响系统的运行速度,下面就针对这个问题的各种解决方案迸行系统介绍。

1.4.1 缓存和页面静态、化

数据量大这个问题最直接的解决方案就是使用缓存,缓存就是将从数据库中获取的结果暂

时保存起来,在下次使用的时候无需重新到数据库中获取,这样可以大大降低数据库的压力。

缓存的使用方式可以分为通过程序直接保存到内存巾和使用缓存框架两种方式。程

序直接操作主要是使用Map ,尤其是ConcurrentHashMap. 而常用的缓存框架有Ehcache 、

Memcache 和Redis 等。缓存使用过程中最重要问题是什么时候创建缓存和缓存的失效机制。

缓存可以在第一次获取的时候创建也可以在程序启动和缓存失效之后立即创建,缓存的失效

可以定期失效,也可以在数据发生变化的时候失效,如果按数据发生变化训:缓存失效,还可

以分粗粒度失效和细粒度失效。

缓存l' I 宅数据的管理方法

如果缓存是在第一次获取的时候创建的,那么在使用缓存的时候最好将没有数据的缓存使用特定的类型值来保存,因为这种方式下如果从缓存中获取不到数据就会从数据库中获取,如果数据库中本来就没有相应的数据就不会创建缓存,这样将每次都会查询数据库, 比如有个专门保存文章评论的缓存,不同的评论按照不同文章的Jd 来保存,如果有一篇文章本来就没有评论,那么就没有相应的续存或者缓存的值为nu ll ,这样程序在每次调用这篇文章的评论时都会查询数据库u 这就没起到缓存的作用,我们可以创建一个专门的类(如N oComment) 来保存没有评论的缓存,这样程序从缓存中查询后就可以知道是还没有创建缓存还是本来就没有评论内容。

跟缓存相似的封外一种技术叫页面静态化,它在原理上跟缓存非常相似,缓存是将从数

据库rl' 获取到的数据(当然也可以是别的任何可以序列化的东两)保存起来,而贞面静态化

是将程序最后~lJ式的丘l面保存起来,使用贞面静态化后就不需要每次调用都重新生成员面(,

这样不但不'rlri~ 盒询数据库, I而且连应用程序处理都省了,所以页面静态化同时对数据量大

和ÌI-发量高两大问题都有好处。

页面静态化可以在程序巾使用模版技术生)JX; , 如常用的Freemarker 和Velocity 都可以根

据模板生成静态贞面,另外也可以使用缓存服务器在应用服务器的上一层缓存生成的页面,

如可以使用Squid ,另外N ginx 也提供了相应的功能。

1.4.2 数据库优化

要解决数据挂大的问题,是避不开数据库优化的。数据库优化可以在不增加硬件的情况

下提高处理效率,这是一种用技术换金钱的方式。数据库优化的方法非常多,常用的有表结

构优化、sQLì吾句优化、分区和分表、索引优化、使用存储过程代替直接操作等,另外有时

候合理使用冗余也能获得非常好的效果

表结构优化

表结构优化是数据库中最基础也是最重要的,如果表结构优化得不合理,就可能导致

严重的性能问题,具体怎么设计更合理也没有固定不变的准则,需要根据实际情况具体

处理。

SQL 语句优化

SQL 语句优化也是非常重要的,基础的SQL 优化是语法层面的优化,不过更重要的是处理逻辑的优化,这也需要根据实际情况具体处理,而且要和索引缓存等配合使用。不过SQL优化有一个通用的做法就是,首先要将涉及大数据的业务的SQ L 语句执行时间详细记录下来,其次通过仔细分析日志(同一条语句对不同条件的执行时间也可能不同,这点也需要仔细分析)找出需要优化的语句和其巾的问题,然后再有的放矢地优化,而不是不分重点对每条语句棉花同样的时间和1精力优化。

分区分|又就是将一张表中的数据按照一定的规则分到不同的区来保存,这样在查询数据

n,J 如果数据的范伟|在同一个区内那么可以只对一个区的数据进行操作,这样操作的数据量更

少,应度更快,而且这种方法对程序是透明的,程序不需要做任何改动。

分表

如果一张表巾的数据吁以分为几种罔定不变的类型,而且如果同时对多种类型共同操作

的怕出不多,那么都可以通过分表来处理

索引优化

索引的大致原理是在数据发生变化(增删改)的时候就预先按指定宇段的顺序排列后保存

到一个类似表的结构巾,这样在查找索引宇段为条件的记录时就可以很快地从索引巾找到对

应记录的指针并从表中获取到记录,这样速度就快多了。不过索引也是一把双刃剑,它在提

高查询速度的同时也降低了增删改的速度,因为每次数据的变化都需要更新相应的索引。

过合理使用索引对提升查询速度的效果非常明显,所以对哪些字段使用索引、使用什么类型

的索引都需要仔细琢磨,并且最好再做一些测鼠。

使用存储过程代替直接操作

在操作过程复杂而且调用频率高的业务中,可以通过使用存储过程代替直接操作来提高

效率,因为存储过程只需要编译一次,而且可以在一个存储过程里面做一些复杂的操作。

上面这些就是经常用到的数据库优化的方法,实际环境中怎么优化还得具体情况具体分

析。除了这些优化方法,更重要的是业务逻辑的优化。

1.4.3 分离活跃数据

虽然有些数据总数据量非常大,但是活跃数据并不多,这种情况就可以将活跃数据单独

保存起来从而提高处理效率。比如,对网站来说,用户很多时候就是这种数据,注册用户很

多,但是活跃用户却不多,而不活跃的用户中有的偶尔也会登录网站,因此还不能删除。这

时就可以通过一个定期处理的任务将不活跃的用户转移到别的数据表中,在主要操作的数据

表中只保存活跃用户,查询时先从默认表中查找,如果找不到再从不活跃用户表中查找,这

样就可以提高查询的效率。判断活跃用户可以通过最近登录时间,也可以通过指定时间段内

登录次数。除了用户外还有很多这种类型的数据,如一个网站上的文章(特别是新闻类的)、

企业业务系统中按时间记录的数据等。

1.4.4 批量读取和延迟修改

计算的逻辑,放在应用程序,还是放在DB中。

如果有for循环的动作,可以将循环的动作转为in的操作,交给数据库来进行一次查询处理。

批量读取和延迟修改的原理是通过减少操作的次数来提高效率,如果使用得恰当,效率

将会呈数量级提升。

批量读取是将多次查询合并到一次中进行,比如,在一个业务系统中需要批量导人工人

信息,在导人前需要检查工人的编码是否已经在数据库中、工人对应的部门信息是否正确(在

部门表中是否存在)、工人的工种信息在工种表中是否存在等,如果每保存一条记录都查询一

次数据库,那么对每个需要检查的宇段,都需要查询与要保存的记录条数相同次数的数据库,

这时可以先将所有要保存的数据的相应字段读取到一个变量中,然后使用III 语句统一查询一

次数据库,这样就可以将n (要保存记录的条数)次查询变为一次查询了。除了这种对同一个

请求中的数据批量读取,在高并发的情况下还可以将多个请求的查询合并到一次进行,如将3

秒或5 秒内的所有请求合并到一起统一查询一次数据库,这样就可以有效减少查询数据库的

次数,这种类型可以用异步请求来处理。

延迟修改主要针对高并发而且频繁修改(包括新增)的数据,如一些统计数据。这种情况

可以先将需要修改的数据暂时保存到缓存中,然后定时将缓存中的数据保存到数据库中,程

序在读取数据时可以同时读取数据库中和缓存中的数据。这里的缓存租前面介绍的缓存有本

质的区别,前面的缓存在使用过程中,数据库中的数据一直是最完整的,但这里数据库中的数据会有一段时间不完整。这种方式下如果保存缓存的机器:P,现了问题将可能会丢失数据,

所以如果是重要的数据就需要做一些特殊处理。笔者之前所在的单位有一个系统需要每月月

末各厂分别导入自己厂当月的相应数据,每到月末那个系统就处于基本瘫痪的状态了,而且

各厂从整理H~ 数据到导入系统只有几天的时间,所以有的厂就专门等晚上人少的时候才进行

操作,对于这种情况就可采用延迟修改的策略来解决。

1.4.5 读写分离

读写分离的本质是对数据库进行集群,这样就可以在高并发的情况下将数据库的操

作分配到多个数据库服务器去处理从而降低单行服务器的压力,不过由于数据库的特殊

性一一每台服务器所保存的数据都需要一致,所以数据同步就成了数据库集群巾最核心的

问题。如果多台服务器都可以写数据那么数据同步将变得非常复杂,所以一般情况下是将写

操作交给专门的一台服务器处理,这台专门负责写的服务器叫做主服务器。当主服务器写入

(增删改)数据后从底层同步到别的服务器(从服务器),读数据的时候到从服务器读取,从服

务器可以有多台,这样就可以实现读可分离,并且将读请求分配到多个服务器处理。主服务

器向从服务器同步数据时,如果从服务器数量多,那么可以让主服务器先向其巾一部分从服

务器同步数据,第一部分从服务器接收到数据后再向另外一部分同步,这时的结构如罔1 -5

简单的数据同步方式可以采用数据库的热备份功能,不过读取到的数据'可能会存在一定

的滞后性,高级的方式需要使用专门的软硬件配合。另外既然是集群就涉及负载均衡问题,

负载均衡和读写分离的操作-般采用钊丁程序处理,而且对应用系统来说是透明的。

技术架构

软件的分层架构

每一个层分别做什么事情

贫血模式--参考16楼的书中说的内容

分布式

Session

Redis

反向代旦旦服务器和代理服务器的区别

代理服务器的作用是代我们获取想要的资源然后将结果返回给我们,所要获取的资源

是我们主动告诉代理服务器的,比如,我们想访问Facebook , 但是直接访问不了, 这时就可以让代理服务器访问,然后将结果返回给我们。

反向代理服务器是我们正常访问一台服务器的时候,服务器自己调用了别的服务器的

资源并将结果返回给我们,我们自己并不知道。

代理服务器是我们主动使用的,是为我们服务的,它不需要有自己的域名;反向代理

服务器是服务器自己使用的我们并不知道, 它有自己的域名,我们访问它跟访问正常的网址没有任何区别。

反向代理服务器可以和实际处理请求的服务器在同一行主机上,而且一行反|句代理服务器也可以访问多合实际处理请求的服务器。反向代理服务器主要有=个作用: ①可以作为前端服务器跟实际处理请求的服务器(如Tomcat) 集成;②可以用做负载均衡;③转发请求.比如,可以将不同类型的资源请求转发到不同的服务器去处理,可以将动态资源转发到Tomcat 、Php 等动态程序而将阁片等静态资源的请求转发到静态资源的服务器,另外也可以在url 地址结构发生变化后将新地址转发到原来的1 1:1地址上。

1.5.5 CDN

CDN 其实是一种特殊的集群页面缓存服务器,它和普通集群的多白页面缓存服务器比主

要是它存放的位置和分配请求的方式有点特殊。CDN 的服务器是分布在全罔各地的.当接收

到用户的请求后会将请求分配到最合适的CDN 服务器节点获取数据,比如,联通的用户会

分配到联通的节点,电信的用户会分配到电信的节点;另外还会愤照地理位置进行分配,北

京的用户会分配到北京的节点,上海的用户会分配到上悔的节点。CDN 的每个节点Jt实就是

一个页面缓存服务器,如果没有请求资源的缓存就会从主服务黯获取,再则直接返回缓布的

贞面。CDN 分配请求的方式比较特殊,它:)i 不是使用普通的负载均衡服务器来分配的,而是

用专门的CDN 域名解析服务器在解析域名的时候就分配好的, 一般的做法是在ISP JJI:I里使用

CNAME 将域名解析到一个特定的域名,然后再将解析到的那个域名用专门的CDN 服务器解

高并发

线程池的应用

高并发中,

Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:

(1)必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。

(2)需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程。

Java高并发应用频繁创建和销毁线程的操作是非常低效的,而且是不被编程规范所允许的。如何降低Java线程的创建成本?必须使用到线程池。线程池主要解决了以下两个问题:

(1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建的线程进行复用,使得性能提升明显。

(2)线程管理:每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。

JUC就是java.util.concurrent工具包的简称,该工具包是从JDK 1.5开始加入JDK的,是用于完成高并发、处理多线程的一个工具包。

在JUC中有关线程池的类与接口的架构图大致如图1-15所示。

各接口的使用方式

1.Executor

Executor是Java异步目标任务的“执行者”接口,其目标是执行目标

任务。“执行者”Executor提供了execute()接口来执行已提交的Runnable执

行目标实例。Executor作为执行者的角色,其目的是提供一种将“任务提

交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:

void execute(Runnable command)

2.ExecutorService

ExecutorService继承于Executor。它是Java异步目标任务的“执行者

服务接”口,对外提供异步任务的接收服务。ExecutorService提供了“接

收异步任务并转交给执行者”的方法,如submit系列方法、invoke系列方

法等,具体如下:

//向线程池提交单个异步任务

<T> Future<T> submit(Callable<T> task);

//向线程池提交批量异步任务

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)

throws InterruptedException;

3.AbstractExecutorService

AbstractExecutorService是一个抽象类,它实现了ExecutorService接

口。AbstractExecutorService存在的目的是为ExecutorService中的接口提

供默认实现。

4.ThreadPoolExecutor

ThreadPoolExecutor就是大名鼎鼎的“线程池”实现类,它继承于

AbstractExecutorService抽象类。

ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止

需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使

用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统

计,方便线程的管理和监控。

5.ScheduledExecutorService

ScheduledExecutorService是一个接口,它继承于ExecutorService。

它是一个可以完成“延时”和“周期性”任务的调度线程池接口,其功能和

Timer/TimerTask类似。

6.ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了

ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象

调度方法的具体实现。

ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,

ScheduledThreadPoolExecutor的性能要优于Timer。

7.Executors

Executors是一个静态工厂类,它通过静态工厂方法返回

ExecutorService、ScheduledExecutorService等线程池示例对象,这些静

态工厂方法可以理解为一些快捷的创建线程池的方法。

Executors的4种快捷创建线程池的方法

Java通过Executors工厂类提供了4种快捷创建线程池的方法,具体

如表1-1所示。

ThreadPoolExecutor线程池的标准创建方式

ThreadPoolExecutor构造方法有多个重载版本,

主要参数有7个

public ThreadPoolExecutor(int corePoolSize,// 核心线程数即使线程空闲(Idle)也不会回收

                              int maximumPoolSize,// 线程数的上限

                              long keepAliveTime,

                              TimeUnit unit,// 线程最大空闲(Idle)时长

                              BlockingQueue<Runnable> workQueue,// 任务的排队队列

                              ThreadFactory threadFactory,// 新线程的产生方式

                              RejectedExecutionHandler handler// 拒绝策略)

}

参数corePoolSize用于设置核心(Core)线程池数量,参数

maximumPoolSize用于设置最大线程数量。线程池执行器将会根据

corePoolSize和maximumPoolSize自动维护线程池中的工作线程,大致规

则为:

(1)当在线程池接收到新任务,并且当前工作线程数少于

corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线

程来处理该请求,直到线程数达到corePoolSize。

(2)如果当前工作线程数多于corePoolSize数量,但小于

maximumPoolSize数量,那么仅当任务排队队列已满时才会创建新线

程。通过设置corePoolSize和maximumPoolSize相同,可以创建一个固定

大小的线程池。

(3)当maximumPoolSize被设置为无界值(如

Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。

(4)corePoolSize和maximumPoolSize不仅能在线程池构造时设

置,也可以调用setCorePoolSize()和setMaximumPoolSize()两个方法进行

动态更改。

3.keepAliveTime

线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置

池内线程最大Idle(空闲)时长(或者说保活时长),如果超过这个时

间,默认情况下Idle、非Core线程会被回收。

2.BlockingQueue

BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,

如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队

列中。

新建线程(ThreadFactory)

Java线程池中的ThreadFactory是用来创建线程的工厂接口,它主要有两个作用:

定义线程的属性:ThreadFactory可以自定义线程的一些属性,如线程名、优先级、是否为守护线程等,从而可以更加灵活地管理线程。

创建线程实例:ThreadFactory负责创建线程实例,这些线程实例将被添加到线程池中,并被用来执行任务。

Java线程池提供了一个默认的ThreadFactory实现类——DefaultThreadFactory,它会创建普通的、非守护线程,并使用简单的编号来命名线程。如果需要自定义线程的属性,可以实现自己的ThreadFactory类并传递给线程池进行使用。ThreadFactory如果调用newThread(Runnable r)方法返回null则表示创建线程失败,但线程池会继续运行但可能不会执行任何任务。

ThreadFactory的作用并不仅限于创建线程实例,它还可以用来记录日志、监控线程状态等,因此在实际开发中,可以根据具体的需求自定义ThreadFactory实现类来满足不同的需求。

阻塞队列(BlockingQueue)

Java线程池中的阻塞队列(Blocking Queue)用于存放等待执行的任务,线程池中的工作线程从队列中取出任务并执行。Java提供了多种类型的阻塞队列,常用的有以下几种:

ArrayBlockingQueue:一个有界的阻塞队列,它的内部实现是一个定长数组。当队列已满时,新的任务将无法加入队列中,直到有工作线程从队列中取出任务。

LinkedBlockingQueue:一个可选有界阻塞队列,在构造函数中可以指定队列的大小。当队列已满时,新的任务将会被阻塞,直到有工作线程从队列中取出任务。

SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的删除操作,否则插入操作将一直阻塞。这种队列通常用于传递数据的场景,例如在线程池中执行RPC调用。

PriorityBlockingQueue:一个支持优先级的无界阻塞队列,元素会按照优先级顺序被取出。如果没有指定优先级,将按照自然排序的顺序进行排序。

阻塞队列的选择取决于应用程序的需求和特性。如果需要控制队列的容量并防止内存泄漏,可以使用ArrayBlockingQueue或LinkedBlockingQueue。如果需要实现多个线程之间的交互,可以使用SynchronousQueue。如果需要按照优先级顺序处理任务,可以使用PriorityBlockingQueue。

RejectedExecutionHandler:饱和策略

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略

AbortPolicy:直接抛出异常,默认策略

CallerRunsPolicy:用调用者所在的线程来执行任务

DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,并将当前任务添加到队列中

DiscardPolicy:直接丢弃任务

也可以自定义饱和策略执行,RejectedExecutionHandler

ScheduledThreadPoolExecutor的创建方式

ScheduledThreadPoolExecutor-构造方法有多个,构造方法其实还是调用ThreadPoolExecutor的构造方法,不同点的是这里的队列是用的DelayedWorkQueue(延迟阻塞队列)。

构造之后,采用父接口的方式调用方法

ScheduledExecutorService方法的定义如下:

public ScheduledFuture scheduleWithFixedDelay(Runnable command,

                                                  long initialDelay,

                                                  long delay,

                                                  TimeUnit unit)

接下来我们讲解ScheduleWithFixedDelay()方法中的关键参数:

1. Runnable command

java.lang.Runnable接口的实现类,表示将要执行的任务。任务将在单独的线程中执行,所以在编写任务时,必须保证线程安全性。

2. long initialDelay

表示任务第一次执行的延迟时间,单位为unit,可以通过该参数实现定时任务的延迟执行。

3. long delay

表示任务执行周期,单位为unit,也就是每次执行任务的时间间隔。注意该参数是以上一次任务执行完毕之后开始计算,而不是任务开始执行的时间。

4. TimeUnit unit

表示时间单位,常用的有TimeUnit.MILLISECONDS, TimeUnit.SECONDS等等。可以通过该参数设置延迟及周期的单位。

例如,在Tomcat中ContainBase.java中

ScheduledThreadPoolExecutor

.scheduleWithFixedDelay(new ContainerBackgroundProcessor(), backgroundProcessorDelay,

                            backgroundProcessorDelay, TimeUnit.SECONDS);

表示任务第一次执行的延迟backgroundProcessorDelay。每次执行任务的间隔(从上一次任务执行完成之后开始计算)为backgroundProcessorDelay。

向线程池提交任务的两种方式

向线程池提交任务的两种方式大致如下:

方式一:调用execute()方法,例如:

void execute(Runnable command);

方式一:调用execute()方法,例如:

//ExecutorService 接口中的方法

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

阿里的规范

4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1)FixedThreadPool SingleThreadPool

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2)CachedThreadPool

允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

3)ScheduledThreadPool

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

ThreadLocal原理

在Java的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的现象。

ThreadLocal类通常被翻译为“线程本地变量”类或者“线程局部变量”类。ThreadLocal位于JDK的java.lang核心包中。如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。ThreadLocal的英文字面意思为“本地线程”,实际上ThreadLocal代表的是线程的本地变量,可能将其命名为ThreadLocalVariable更加容易让人理解。

ThreadLocal如何做到为每个线程存有一份独立的本地值呢?一个ThreadLocal实例可以形象地理解为一个Map(早期版本的ThreadLocal是这样设计的)。当工作线程Thread实例向本地变量保持某个值时,会以“Key-Value对”(即键-值对)的形式保存在ThreadLocal内部的Map中,其中Key为线程Thread实例,Value为待保存的值。当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其绑定的Value。

Java程序可以调用ThreadLocal的成员方法进行本地值的操作,具体的成员方法如表1-2所示。

ThreadLocal的使用场景

ThreadLocal使用场景介绍以及关于内存泄漏的探讨_threadlocal 写一段代码导致内存泄露-CSDN博客  参考

ThreadLocal是解决线程安全问题的一个较好的方案,它通过为每个线程提供一个独立的本地值去解决并发访问的冲突问题。在很多情况下,使用ThreadLocal比直接使用同步机制(如synchronized)解决线程安全问题更简单、更方便,且结果程序拥有更高的并发性。

ThreadLocal的使用场景大致可以分为以下两类:

(1)线程隔离

ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。

ThreadLocal在线程隔离的常用案例为:可以为每个线程绑定一个用户会话信息、数据库连接、HTTP请求等,这样一个线程所有调用到的处理函数都可以非常方便地访问这些资源。

常见的ThreadLocal使用场景为数据库连接独享、Session数据管理等。

Hibernate的案例

在“线程隔离”场景中,使用ThreadLocal的典型案例为:可以为每个线程绑定一个数据库连接,使得这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常问题。

下面的代码来自Hibernate,代码中通过ThreadLocal进行数据库连

接(Session)的“线程本地化”存储,主要的代码如下:

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {

Session s = (Session) threadSession.get();

try {

if (s == null) {

s = getSessionFactory().openSession();

threadSession.set(s);

}

} catch (HibernateException ex) {

throw new InfrastructureException(ex);

}

return s;

}

Hibernate对数据库连接进行了封装,一个Session代表一个数据库连接。通过以上代码可以看到,在Hibernate的getSession()方法中,首先判断当前线程中有没有放进去Session,如果还没有,那么通过sessionFactory().openSession()来创建一个Session,再将Session设置到ThreadLocal变量中,这个Session相当于线程的私有变量,而不是所有线程共用的,显然其他线程中是取不到这个Session的。

复杂场景

有个UserService类,实现一个功能,通过用户id,拿到用户的生日

public String birthDate(int userId) {

        Date birthDate = getBirthDay(userId);

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

        return sdf.format(birthDate);

    }

方法一:

如果新建10个线程,每个线程都在运行[方法birthDate],那么相当于每个线程都会新建一个df

存在的问题:如果我们有1000个task需要执行,那么直接创建1000个线程,创建了1000个df对象,显然不太合理,通常情况下,我们会用线程池的方式执行任务。

方法二:

我们新建一个核心线程数为10的线程池,往里面提交1000个任务。我们使用了线程池的方式,线程数为10,但会创建1000个df对象

方式三 :

将SimpleDateFormat提取到方法birthDate的外面,然后用参数的形式传入。这样解决了每次都会创建df对象的开销,但是SimpleDateFormat是线程不安全的,即需要对这个对象加锁以保证线程安全。

方式3导致的问题:给全局的SimpleDateFormat加锁,会使得同一时间只有一个线程能拿到这个对象,导致效率低下。

那么有没有一种更折中的方案,即既不需要在方法内创建df以致于极端情况下要多达1000次的创建,也不要只有1个df对象,以至于每个线程用到它的时候都要排队拿?

用有限的空间换时间;之前也是空间换时间,但是1000个对象换了时间。现在是线程数量的空间。

使用ThreadLocal,希望每个线程有自己的df对象,这样既不需要每个task都创建一次(节省了开销),也不需要每个thread相互抢一个df(提高了效率):

什么是ThreadLocal:如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。

public class DateFormatThreadLocalUtils {

    public static final ThreadLocal<SimpleDateFormat> df = new ThreadLocal<>() {

        @Override

        protected SimpleDateFormat initialValue() {

            System.out.println("new SimpleDateFormat.....");

            return new SimpleDateFormat("yyyy-MM-dd");

        }

    };

}

这个是jdk8+的lamda的写法

public class DateFormatThreadLocalUtils {

    public static ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

}

UserService类:

可以看到birthDate中使用到的sdf是从ThreadLocal中拿到的。

如果上述的ThreadLocal没有重写initialValue方法,那么在使用的时候可以先判断从ThreadLocal get出来的SimpleDateFormat是否为空,如果为空,再new,再set回ThreadLocal中也是可以的。

public class UserService {

     public String birthDate(int userId) {

        Date birthDate = getBirthDay(userId);

        SimpleDateFormat sdf = DateFormatThreadLocalUtils.df.get();

        return sdf.format(birthDate);

    }

     public Date getBirthDay(int userId) {

        // todo, return a Date

    }

 }

测试:Task有1000个,核心线程数为10,那么上述的SimpleDateFormat只会new 10次,因为它是每个线程独有的。

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class UserServiceMain {

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 1000; i ++) {

            executorService.submit(() -> {

                String birthDate = (new UserService()).birthDate(101);

                System.out.println(Thread.currentThread().getName() + ": " + birthDate);

            });

        }

    }

}

(2)跨函数传递数据

通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。由于ThreadLocal的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写ThreadLocal变量的线程本地值,从而可以方便地实现跨函数的数据传递。使用ThreadLocal保存函数之间需要传递的数据,在需要的地方直接获取,也能避免通过参数传递数据带来的高耦合。在“跨函数传递数据”场景中使用ThreadLocal的典型案例为:可以为每个线程绑定一个Session(用户会话)信息,这样一个线程所有调用到的代码都可以非常方便地访问这个本地会话,而不需要通过参数传递。

假设我们有个API,从前端接收到request,然后经过一系列个service,但每个service都需要user这个参数:每个service的方法都带上user这个参数,以此来传递

也可以采用ThreadLocal方法,然后在第1个service中将user值set到ThreadLocal中,往后的service就可以直接从ThreadLocal中获取。降低代码的耦合性,有点像总线型方式。

public class UserThreadLocalUtils {

    public static final ThreadLocal<String> USER_ID_HOLDER = new ThreadLocal<>();

}

比如我们写一个Filter,将userId存放到ThreadLocal中:

@Component

public class UserFilter implements Filter {

    @Override

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        try {

            HttpServletRequest request = (HttpServletRequest)servletRequest;

            String userId = request.getHeader("userId");

            UserThreadLocalUtils.USER_ID_HOLDER.set(userId);

            filterChain.doFilter(servletRequest, servletResponse);

        } finally {

            UserThreadLocalUtils.USER_ID_HOLDER.remove();

        }

    }

}

那么我们在Controller或是Service中都可以从ThreadLocal中拿:

@Slf4j

@RestController

public class UserController {

     @Autowired

    private UserService userService;

     @GetMapping("user")

    public boolean get() {

        log.info("Get userId = {}", UserThreadLocalUtils.USER_ID_HOLDER.get());

        userService.get();

        return true;

    }

 }

@Slf4j

@Service

public class UserService {

     public void get() {

        log.info("Get userId = {}", UserThreadLocalUtils.USER_ID_HOLDER.get());

    }

}

ThreadLocal导致的内存泄漏问题

什么是内存泄漏?不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏

衡量性能的指标QPS/TPS等

理解 QPS(Queries Per Second)、TPS(Transactions Per Second)和 RT(Response Time)之间的关系对于评估和优化系统的性能至关重要。下面详细解释这三个指标及其相互关系。

1. QPS (Queries Per Second)

定义:每秒钟处理的查询次数。

用途:衡量服务器处理请求的能力。

例子:HTTP GET、POST 请求等。

是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。

2. TPS (Transactions Per Second)

定义:每秒钟处理的事务次数。

用途:衡量系统的业务处理能力。

例子:一次登录操作、一次订单提交等。

3. RT (Response Time)

定义:处理一个请求所需的时间。

用途:衡量系统的响应速度。

例子:从客户端发送请求到收到响应的时间。

关系分析

QPS 和 TPS

一个事务可能涉及多个请求:

一次登录操作可能涉及多个请求,如验证用户名、密码、生成会话等。

一次订单提交 TPS 可能涉及多个请求QPS,如检查库存、扣减库存、生成订单等。

QPS > TPS:

由于一个事务可能涉及多个请求,因此 QPS 通常会大于 TPS。

例如,一次登录操作可能涉及 2-3 个请求,那么 QPS 通常是 TPS 的 2-3 倍。

QPS 和 RT

RT 对 QPS 的影响:

如果 RT 较长,系统在同一时间内能够处理的请求较少,因此 QPS 较低。

如果 RT 较短,系统在同一时间内能够处理更多的请求,因此 QPS 较高。

RT 和系统性能:

RT 越短,用户体验越好。

RT 越长,用户体验越差,可能导致用户流失。

TPS 和 RT

RT 对 TPS 的影响:

如果 RT 较长,系统在同一时间内能够处理的事务较少,因此 TPS 较低。

如果 RT 较短,系统在同一时间内能够处理更多的事务,因此 TPS 较高。

RT 和系统负载:

当系统负载较高时,RT 可能变长。

优化系统性能可以缩短 RT,从而提高 TPS。

并发数

同时访问服务器站点的连接数。

建议参考文章:你,真的理解什么是并发数吗? - 小文叔 - 博客园

六、并发连接数

(The number of concurrent connections)

并发连接数就是服务器某个时刻所接受的请求数目,也就是某个时刻所接受的会话数目。

七、并发用户数

(The number of concurrent users, Concurrent Level)

一个用户可能产生多个会话,所以并发用户数和并发连接数并不重复。并发用户数是指服务器某个时刻所能接受的用户数。

并发工具的使用

Jmeter

https://blog.51cto.com/u_92655/10756342

自定义Java代码的测试

持续集成

比较复杂的云原生的Zadig,之前的docker单机在前面已经讲过

软件交付是复杂且具有挑战性的过程,涉及到人员、技术、流程和工具等要素。在这个过程中,企业常常遇到以下问题:开发环境模拟困难,多业务联调困难,研发效率低下;大量手工测试效率低,环境不稳定,自动化建设困难;运维负担重工具繁多,手工操作繁重,交付效率低;跨部门沟通困难,流程制定耗时……为应对这些难题,Zadig 通过平台工程和技术升级提升组织效能,建立全流程一体化工程协同基线,帮助企业更高效地进行软件交付,释放团队生产力。

软件工程化时代已然到来。

Zadig 提供工程底座,产研团队可统一协作实现敏捷交付,完成需求从开发到测试到发布的全生命周期。并且支持自定义流程,扩展工具,编排测试服务、IT 服务、安全服务等能力,通过 Zadig 可以自动化一切,让工程师专注在创造上。

Zadig 是由 KodeRover 公司基于 Kubernetes 研发的自助式云原生 DevOps 平台,源码 100% 开放。Zadig 提供灵活可扩展的工作流支持、多种发布策略编排以及一键安全审核等特性。该平台还支持定制的企业级 XOps 敏捷效能看板,深度集成多种企业级平台,并通过项目模板化批量快速接入,实现数千个服务的一键纳管治理。其主要目标是帮助企业实现产研的数字化转型,使工程师成为创新引擎,并为数字经济的无限价值链接提供支持。

核心能力

灵活易用的高并发工作流

简单配置,可自动生成高并发工作流,多个微服务可并行构建、并行部署、并行测试,大大提升代码验证效率。自定义的工作流步骤,配合人工审批,灵活且可控的保障业务交付质量。

面向开发者的云原生环境

分钟级创建或复制一套完整的隔离环境,应对频繁的业务变更和产品迭代。基于全量基准环境,快速为开发者提供一套独立的自测环境。一键托管集群资源即可轻松调试已有服务,验证业务代码。

高效协同的测试管理

便捷对接 Jmeter、Pytest 等主流测试框架,跨项目管理和沉淀 UI、API、E2E 测试用例资产。通过工作流,向开发者提供前置测试验证能力。通过持续测试和质量分析,充分释放测试价值。

强大免运维的模板库

跨项目共享 K8s YAML 模板、Helm Chart 模板、构建模板、工作流模板等,实现配置的统一化管理。基于一套模板可创建数百微服务,开发工程师少量配置可自助使用,大幅降低运维管理负担。

安全可靠的发布管理

自定义工作流打通人、流程、内外部系统合规审批,支持灵活编排蓝绿、金丝雀、分批次灰度、Istio 等发布策略。通过多集群、多项目视角呈现生产环境的状态,实现发布过程的透明可靠。

稳定高效的客户交付

简化供应商对客户版本、客户私有云、公有云、离线环境的产品实施过程以及产品许可证的管理。供应商管理平面联动客户控制台完成对客户环境实施、更新、维护过程,提升企业对外服务质量。

客观精确的效能洞察

全面了解系统运行状态,包括集群、项目、环境、工作流,关键过程通过率等数据概览。提供项目维度的构建、测试、部署等客观的效能度量数据,精准分析研发效能短板,促进稳步提升。

云原生 IDE 插件

开发者无需平台切换,在 VScode IDE 中即可获得 Zadig 产品核心能力。编写代码后,无需打包镜像,即可一键热部署到自测环境,快速完成自测、联调和集成验证,开发效率倍增。

支持从需求到发布全流程敏捷交付。尤其面向多服务并行部署发布,云原生构建环境和运行环境,基础设施对接及企业级 SSO/权限管理等

专门面向开发者的生产力平台,涵盖需求到开发,测试,运维的云原生一体化技术底座支撑

在软件开发和部署的过程中,为了确保应用的质量与稳定性,通常会设置多个环境来分别承担不同的职责。以下是对常见的几种环境的说明:

PROD(Production):生产环境,也称为线上环境或正式环境。这是应用程序实际运行的地方,面向最终用户,处理真实的数据和流量。生产环境管理,变更过程需经过严格审批。

SIT(System Integration Testing):系统集成测试环境。主要用于不同系统间的集成测试,验证各个子系统能否正确地交互工作。

UAT(User Acceptance Testing):用户验收测试环境。此环境用于模拟生产环境,让最终用户或客户进行验收测试,确认系统是否满足业务需求。

TEST(Testing):测试环境。广义上涵盖了所有类型的测试活动,包括但不限于单元测试、集成测试等。它可能包含了多个细分的测试环境。

PRE(Pre-production):预生产环境,有时也被称作准生产环境。这个环境尽可能接近生产环境,用来做最后的测试,确保新版本上线前没有重大问题。

DEV(Development):开发环境。每个开发者在本地或共享服务器上使用的环境,用于编写和调试代码。

FAT(Functionality Acceptance Testing):功能验收测试环境。专注于验证系统的功能性需求是否得到满足,类似于UAT,但更侧重于技术层面的功能实现。

每个组织可能会根据自己的实际情况调整这些环境的具体配置和命名方式,但上述环境的基本用途是比较通用的。在实际操作中,合理地利用这些环境可以帮助团队更好地管理项目的生命周期,提高软件质量。

测试

日志监控

通知

冲突的处理

微服务

部署,docker,git,版本化

Docker持续集成

测试先行,测试数据

结对编程

敏捷开发

迭代更新的理念

非功能性特征

在开发过程中,有80%的工作用于非功能性特征。如果有低代码或者无码开发的,那么80%的工作就可以节省下来。

微服务与单体化考虑问题的点差异

配置文件的位置

配置文件的更新方式

心跳的设置

用户暴露的服务方式

用户的路由设置

用户的访问量的控制

权限的设置

Log的设置

微服务的启动顺序

启动Nacos,如果后启动,先启动的服务无法注册

启动Nacos的时候,可以有2种启动方式

一种是自带的jar文件启动。

一种是自己单独构建一个springboot的项目,依赖nacos,然后在打成一个jar包。这种方式,适合与跟踪nacos的原理

在Nacos中,有配置管理,服务管理

Nacos本身带有db,将配置的内容,初始化到db中

然后继续启动

FAQ

JeecgCloud之后,权限的设置在什么地方?如何获取到Token?

Jeecg的微服务

依赖Docker desktop

在desktop上启动mysql,不要用本机的mysql

更新一下hosts的文件,访问dns的访问

Redis采用外网的

Nacos 采用idea开发环境的代码启动

Ruoyi的微服务

参考2个文档

file:///F:/ruoyi/%E8%AF%BE%E4%BB%B6/%E8%8B%A5%E4%BE%9D(ruoyi-cloud)%E8%84%9A%E6%89%8B%E6%9E%B6%E8%A7%A3%E8%AF%BB.md

http://doc.ruoyi.vip/ruoyi-cloud/cloud/skywalking.html#如何使用

Mysql 采用本地的 net start mysql80

Nacos采用已经好的jar启动

F:\ruoyi\nacos-server-2.2.3\nacos\bin

执行

startup.cmd -m standalone

程序的入口

服务的入口

这个是产生验证码的地方

所有的地方,都需要token,除了code及whites的地址不需要token

代码生成插入到system的服务中

启动代码服务

然后有一个emloyee的表,参考上面连接的markdown的文档

代码生成之后,各个文件,单独的插入到不同的分层中,这个不方便将其一次性复制到新的地方。所以这种分层的方式,感觉不好。

新增微服务分层的处理

技术组件的名称

OpenFeign 全称 Spring Cloud OpenFeign,它是 Spring 官方推出的一种声明式服务调用与负载均衡组件,它的出现就是为了替代进入停更维护状态的 Feign。OpenFeign 是 Spring Cloud 对 Feign 的二次封装,它具有 Feign 的所有功能,并在 Feign 的基础上增加了对 Spring MVC 注解的支持,例如 @RequestMapping、@GetMapping 和 @PostMapping 等。

Feign 与 OpenFeign

4.1.2 OpenFeign与Ribbon

OpenFeign具有负载均衡功能,其可以对指定的微服务采用负载均衡方式进行消费、访问。之前老版本Spring Cloud所集成的OpenFeign默认采用了Ribbon负载均衡器。但由于Netflix已不再维护Ribbon,所以从Spring Cloud 2021.x开始集成的OpenFeign中已彻底丢弃Ribbon,而是采用Spring Cloud自行研发的Spring Cloud Loadbalancer作为负载均衡器。

Dubbo3

FAQ

SpringMVC的路由代码 与SpringGateWay的路由代码的差异

权限的Token是什么时候代入到客户端的?

答:

Code是先。不需要进行安全论证

Login之后,产生access_token

那么,它的安全的注解在什么进行过滤的? 感觉auth只是一个token产生,但是并没有安全的过滤的处理?

服务相互之间的调用处理方式? 是否也有权限进行控制

单独写一个微服务,然后由别的服务来进行调用。而不仅仅是客户端来进行调用。如果这个服务上也有db操作。那么就会出现分布式事务的操作。

采用远程调用与resttemplate的调用差别

被调用方不动。调用方采用resttemplate

如果采用feign。采用一个中间的内容

调用方,采用 类似 本地方法进行调用。比较简化

被调用方,采用接口封装

采用fallbackfactory

采用构建一下新的微服务

采用 sys_empcloud一个表。然后代码自动生成。产生一个module

在system模块下,调用了

FeignClient注解的含义

后面要查一下这个原因,如何将一个微服务,在外部能进行调用。这样就比较完全了。

@FeignClient (value=“对应的服务名称--在Nacos上的服务名称”)

Public interface TestService {

@GetMapping(“/payment/list”)  //这个url是对方服务的两者的拼接

Result list();

}

https://zhuanlan.zhihu.com/p/409184804

调用方定义一个接口,类似与客户端的sdk。

实现不在调用方

并且它内嵌了ribbon,客户端的均衡

Spring Cloud Loadbalancer作为客户端的负载均衡组件,它代替了ribbon

Feign 是⼀个声明式的 HTTP 客户端组件,它旨在是编写 Http 客户端变得更加容易。

OpenFeign 同时集成了 Spring Cloud LoadBalancer 和 Spring Cloud CircuitBreaker,提供负载均衡和熔断降级的功能。

Feign 默认的负载均衡策略是轮询调用。

之前的远程调用

RestTemplate template = new RestTemplate();

User user = template.getForObject("http://userservice/user/"+uid, User.class);

采用OpenFeign之后,分为2个步骤

定义接口

@FeignClient("userservice")

public interface UserClient {

   //路径保证和其他微服务提供的一致即可

    @RequestMapping("/user/{uid}")

    User getUserById(@PathVariable("uid") int uid);  //参数和返回值也保持一致

}

④service业务中调用客户端接口

我们直接注入使用(有 Mybatis 那味了):

Loadbalance与OpenFeign的集成

spring cloud Alibaba 集成openfeign 和 loadbalancer_openfeign loadbalancer-CSDN博客

对于任何一个大型系统,负载均衡器都是必不可少的设施。以前,负载均衡器大多只部署在整个服务集群的前端,将用户的请求分流到各个服务进行处理,这种经典的部署形式现在被称为集中式的负载均衡。随着微服务日渐流行,服务集群的收到的请求来源不再局限于外部,越来越多的访问请求是由集群内部的某个服务发起,由集群内部的另一个服务进行响应的,对于这类流量的负载均衡,既有的方案依然是可行的,但针内部流量的特点,直接在服务集群内部消化掉,肯定是更合理更受开发者青睐的办法。由此一种全新的、独立位于每个服务前端的、分散式的负载均衡方式正逐渐变得流行起来,这就是本节我们要讨论的主角:客户端负载均衡器(Client-Side Load Balancer),如图 7-4 所示:

疯狂Java的微服务,秒杀的系统

实际案例

举一个例子,快速开发

采用领域建模,采用代码生成器

然后分裂为微服务

实际案例

需求分析

类图构建

代码生成,采用JeecgBoot,不需要重复发明轮子。开源版本中,没有包括工作流。自己集成也可以。

根据业务自动生成。在文档中,不需要多说明开源的JeecgBoot的内容。

开发环境准备

快速搭建第一个SpringBoot的项目

Mybatis中乐观锁的方式--通过拦截器的原理? 那么Jpa有没有什么是通过拦截器的?

Spring4的书中

JeecgBoot中的乐观锁的方式

mybatis-plus就是mybatis的一个增强实现而已。

对mybatis进行了封装,

比如:抽象了通用的增删改查的方法,如果你要用mybatis去做你就要每个都自己在具体写一遍。

其他的就和mybatis没太多的差别了。

业务数据分析

页面数据建模:根据页面来进行建模,每个页面对应1张或多张表

服务数据建模:根据服务来进行建模,表的数量少于页面的,用视图的方式进行呈现出来。

前段代码

Vue页面展示分为查询区,按钮区,列表区

在代码方面,template的模板,data,css

服务端代码

事实上的标准

Controller,Service,等

单表有增删改查的几个6个接口

把变化量作为Map元素管理起来。

模拟数据

参考胡星的文档

异步调用

像RMI和Hessian/Burlap这样的远程调用机制是同步的。如图17.1所示,当客户端调用远程方法时,客户端必须等到远程方法完成后,才能继续执行。即使远程方法不向客户端返回任何信息,客户端也要被阻塞直到服务完成。

消息是异步发送的,如图17.2所示,客户端不需要等待服务处理消息,甚至不需要等待消息投递完成。客户端发送消息,然后继续执行,这是因为客户端假定服务最终可以收到并处理这条消息。

相对于同步通信,异步通信具有多项优势

在异步消息中有两个主要的概念:消息代理(message broker)和目的地(destination)。当一个应用发送消息时,会将消息交给一个消息代理。消息代理实际上类似于邮局。消息代理可以确保消息被投递到指定的目的地,同时解放发送者,使其能够继续进行其他的业务。

尽管不同的消息系统会提供不同的消息路由模式,但是有两种通用的目的地:队列(queue)和主题(topic)。每种类型都与特定的消息模型相关联,分别是点对点模型(队列)和发布/订阅模型(主题)

点对点消息模型

在点对点模型中,每一条消息都有一个发送者和一个接收者,如图

17.3所示。当消息代理得到消息时,它将消息放入一个队列中。当接收者请求队列中的下一条消息时,消息会从队列中取出,并投递给接收者。因为消息投递后会从队列中删除,这样就可以保证消息只能投递给一个接收者。

尽管消息队列中的每一条消息只被投递给一个接收者,但是并不意味着只能使用一个接收者从队列中获取消息。事实上,通常可以使用几个接收者来处理队列中的消息。不过,每个接收者都会处理自己所接收到的消息。

这与在银行排队等候类似。在等待时,我们可能注意到很多银行柜员都可以帮助我们处理金融业务。在柜员帮助客户完成业务后,她就空闲了,此时,她会要求排队等候的下一个人前来办理业务。如果我们排在队伍的最前边时,我们就会被叫到,然后由其中的一个空闲柜员来帮助我们处理业务,而其他的柜员则会帮助其他的银行客户。

发布订阅消息模型

在发布—订阅消息模型中,消息会发送给一个主题。与队列类似,多个接收者都可以监听一个主题。但是,与队列不同的是,消息不再是只投递给一个接收者,而是主题的所有订阅者都会接收到此消息的副本,如图17.4所示。

因为对于异步消息来讲,发布者并不知道谁订阅了它的消息。发布者只知道它的消息要发送到一个特定的主题——而不知道有谁在监听这个主题。也就是说,发布者并不知道消息是如何被处理的。

17.1.2 评估异步消息的优点

虽然同步通信比较容易理解,建立起来也很简单,但是采用同步通信机制访问远程服务的客户端存在几个限制,最主要的是:

同步通信意味着等待。当客户端调用远程服务的方法时,它必须等待远程方法结束后才能继续执行。如果客户端与远程服务频繁通信,或者远程服务响应很慢,就会对客户端应用的性能带来负面影响。

客户端通过服务接口与远程服务相耦合。如果服务的接口发生变化,此服务的所有客户端都需要做相应的改变。

客户端与远程服务的位置耦合。客户端必须配置服务的网络位置,这样它才知道如何与远程服务进行交互。如果网络拓扑进行调整,客户端也需要重新配置新的网络位置。

客户端与服务的可用性相耦合。如果远程服务不可用,客户端实际上也无法正常运行。

虽然同步通信仍然有它的适用场景,但是在决定应用程序更适合哪种通信机制时,我们必须考量以上的这些缺点。如果这些限制正是你所担心的,那你可能很想知道异步通信是如何解决这些问题的。

无需等待

当使用JMS发送消息时,客户端不必等待消息被处理,甚至是被投递。客户端只需要将消息发送给消息代理,就可以确信消息会被投递给相应的目的地。

因为不需要等待,所以客户端可以继续执行其他任务。这种方式可以有效地节省时间,所以客户端的性能能够极大的提高。

面向消息和解耦与面向方法调用的RPC通信不同,发送异步消息是以数据为中心的。

这意味着客户端并没有与特定的方法签名绑定。任何可以处理数据的队列或主题订阅者都可以处理由客户端发送的消息,而客户端不必了解远程服务的任何规范。

位置独立

同步RPC服务通常需要网络地址来定位。这意味着客户端无法灵活地适应网络拓扑的改变。如果服务的IP地址改变了,或者服务被配置为监听其他端口,客户端必须进行相应的调整,否则无法访问服务。

与之相反,消息客户端不必知道谁会处理它们的消息,或者服务的位置在哪里。客户端只需要了解需要通过哪个队列或主题来发送消息。

因此,只要服务能够从队列或主题中获取消息即可,消息客户端根本不需要关注服务来自哪里。

(在点对点模型中,可以利用这种位置的独立性来创建服务的集群。如果客户端不知道服务的位置,并且服务的唯一要求就是可以访问消息代理,那么我们就可以配置多个服务从同一个队列中接收消息。如果服务过载,处理能力不足,我们只需要添加一些新的服务实例来监听相同的队列就可以了。

在发布-订阅模型中,位置独立性会产生另一种有趣的效应。多个服务可以订阅同一个主题,接收相同消息的副本。但是每一个服务对消息的处理逻辑却可能有所不同。例如,假设我们有一组服务可以共同处理描述新员工信息的消息。一个服务可能会在工资系统中增加该员工,另一个服务则会将新员工增加到HR门户中,同时还有一个服务为新员工分配可访问系统的权限。每一个服务都基于相同的数据(都是从同一个主题接收的),但各自进行独立的处理。)

确保投递

为了使客户端可以与同步服务通信,服务必须监听指定的IP地址和端口。如果服务崩溃了,或者由于某种原因无法使用了,客户端将不能继续处理。

但是,当发送异步消息时,客户端完全可以相信消息会被投递。即使在消息发送时,服务无法使用,消息也会被存储起来,直到服务重新可以使用为止。

现在,我们已经对异步消息的基础知识有所了解,接下来看一下如何将其付诸实施。首先,我们会使用JMS来发送和接收消息。

17.2使用JMS发送消息

Java消息服务(Java Message Service ,JMS)是一个Java标准,定义了使用消息代理的通用API。在JMS出现之前,每个消息代理都有私有的API,这就使得不同代理之间的消息代码很难通用。但是借助JMS,所有遵从规范的实现都使用通用的接口,这就类似于JDBC为数据库操作提供了通用的接口一样。

Spring通过基于模板的抽象为JMS功能提供了支持,这个模板也就是JmsTemplate。使用JmsTemplate,能够非常容易地在消息生产方发送队列和主题消息,在消费消息的那一方,也能够非常容易地接收这些消息。

同时,Spring提供了统一的异常处理。将

对于JMS API来说,JMSException的确提供了丰富且具有描述性的子类集合,让我们更清楚地知道发生了什么错误。不过,所有的JMSException异常的子类都是检查型异常,因此必须要捕获。JmsTemplate为我们捕获这些异常,并重新抛出对应非检查型JMSException异常的子类。

客户端发过去之后,到broker。然后另外一个也作为客户端,是主动获取,还是被动等待?

Broker会在作为客户端来进行处理吗?还是一直作为服务端等待用户注册进来。构建长连接?

客户端,Broker,服务器或客户端

连接的方式,采用http ,or websocket?

主要关注

连接的协议

心跳的方式

协议包的解析方式,序列化的方式

采用的框架—是否跨平台

具体的配置项

具体的编程接口方式

消费的方式,等待拉或者主动推送

异步发送消息

异步接受消息

发送之后,通知,不需要等待结果。

发送之后,如果是TCP连接,对方一定给一个结果。但是不是对消息的复杂处理结果,只是收到了。

消息队列解决

发送之后,要有结果。

异步发送,然后返回一个ID。客户端继续处理。

给出回调地址,在发送的时候,就给出。

或者是定时轮询。

消息发送的时候

目的地点:

消息格式转换

Java类转为JSON格式或XML格式。然后在进行序列化。一般情况下,Spring框架都会准备好这些转换器。

Spring还提供了消息驱动POJO的理念:这是一个简单的Java对象,它能够以异步的方式响应队列或主题上到达的消息。复杂的内容,交给框架来进行处理。

异步消息通信与同步RPC相比有几个优点。间接通信带来了应用之间

的松散耦合,因此减轻了其中任意一个应用崩溃所带来的影响。此

外,因为消息转发给了收件人,因此发送者不必等待响应。在很多情况下,这会提高应用的性能。

虽然JMS为所有的Java应用程序提供了异步通信的标准API,但是它

使用起来很繁琐。Spring消除了JMS样板式代码和异常捕获代码,让

异步消息通信更易于使用。

在本章中,我们了解了Spring通过消息代理和JMS建立应用程序之间

异步通信的几种方式。Spring的JMS模板消除了传统的JMS编程模型所必需的样板式代码,而基于Spring的消息驱动bean可以通过声明bean的方法允许方法响应来自于队列或主题中的消息。我们同样了解了如何通过Spring的JMS invoker为Spring bean提供基于消息的RPC。

如果不是发送消息,而是需要返回结构,如果处理?

在运营商的网络中,经常有多个服务之间的调用情况。所以,同步异步非常重要

发送端,有2种方式,一种是异步调用,一种是同步调用。

如果发送端是同步调用,那么需要服务端返回后,同步才能继续进行下去。

这个时候,可以协商服务端立即返回。然后在服务器启动一个新的线程池来完成这个工作。

发送端,然后采用定时任务来查询服务端当前的进展。

如果发送端进行异步调用

那么发送之后,客户端不依赖服务器的实现,立即返回执行后续的动作。

然后开启定时任务请求服务器,查看状况。

这种方式,比上面要更加好一些,避免了在调用的时候对服务端的依赖。

发送端进行异步调用,同时让服务器

简道云中的

前端事件,可以访问外部的一个服务,已经在使用。采用同步等待的方式,但是设置了重试次数及每次重试的时间20s,如果都失败,就放弃。

WebHook,是客户端,当发送一些数据变化的时候,推送一些数据。把服务器的地址的地址写在登记的界面中。它不需要返回值,采用异步发送的方式。不需要客户端这边等待。

API

是简道云的服务器,在处理数据的时候,可以调用Server的api,来添加数据。

架构的分类

撇开市上招聘位的分类,单纯从技角度来看,把件系自底往上分,通常会得

到如1-1 所示的件系架构分

1. 第一:架构

架构指云平台、操作系、网、存、数编译器等随着目前云算越来越

普及 很多的中小型公司都选择了大公司算平台,而不是自己研维护架构

2. 第二:件与大数据平台

(1)件架构。例如分布式服件、消息中、数据件、存中件、

控系、工作流引擎和规则引擎等

(2) 大数据架构。例如开源的Hadoop 体系, Hive Spark Storrn Flink 等。

3. 第三:业务架构

架构1 [

架构

1 - 1 件系架构分

大数据

架构

(1)通用件系。例如最常用的件、浏览器、播放器等

(2)线业务例如各种基于大数据的BI 分析、数据挖掘、表与可化等

(3)大型在线业务例如搜索、推荐、即通信、商、游、广告、企ERP

CRM 等。

于架构的种分类方法,有两点需要:

· 于中小型公司,可能没有第 或者即使有第,也只有很小的部分于大

型公司,在第和第理策略上也不: 有些公司会让业务团队做第

的工作, 11故在线业务的同做了中件的作,做大数据业务的同搭建和

维护数据架构: 有些公司会安排门的团队做中件与数据 供上各个

使用。当然,实际也没有绝对,某个业务团队如果得中团队所做的工

作不能业务需求 可能会选择自己造

·于第的划分,此并不是很绝对 为现实件的种类在太多,比如嵌入式

,通用性件和业务软件的界限也并不是渭分明的。一业务随着技

步,很功能将被通用化、准化,最终变成了个通用系比如搜索,在以

前是专业性很强的业务,随搜索技的不断在搜索的很功能已

被通用化了,有了E S 这样的搜索平台,可以服商、广告等其他各种业务,而不

限于搜索本身再比如权限控制、作流引擎、规则引擎,以前只是在某个业务

里使用后来大家发现很多业务里都需要类似的西,于是抽象出来,成了通用

业务通用化的程是一个技不断步的程,也是个使用门不断被降

低的聚焦在大型在线业务的架构, l{[J1 -1 中第的第部分于大模的在线

业务,一方面要理高并、高可用等技术问题; 方面要面各种复业务需求,

并且些需求如何把业务和技很好地合起来,理好两者的关系,是本

要重点探个方向

但本并不只,相反,要从第层讲只有下面的原理有了深刻理解,

才可能面所构建的业务有深刻认识

但需要面现实是,本不可能同把基架构和业务架构得很透。一方面,作者

不是基架构方面的:方面,任何一个基架构的系,或者中件、大数据的系

都是个很专业的子域,钻进去都可以耗尽个人生的精力

业务架构

技术架构

数据架构ER关系

软件开发,要关注的变化点分析

Separation of Concerns

工程化的处理方式

低代码平台使用

业务人员:

分析表

低代码平台与原型字段一样

生成前后端系统

前端开发人员

做前端页面

做前端页面的数据转换,从List中进行数据转换

后端开发人员

不允许修改生成的代码

新增一个Controller的,复制原来的list方法

增加一个版本号

在客户端没有任何变化的情况下,选择最新的版本。

这样只要在服务端自己写的Controller方法中,增加@ApiVersion(1),就可以访问到自己写的controller中。可以保持url一样。甚至可以写好几个方法,@ApiVersion(2) ,@ApiVersion(3),这样可以修改若干次。但是总数请求最新的一个。除非客户端指定具体的版本号

查询的方法

@AutoLog(value = "SeachdownTest-分页列表查询")

@ApiOperation(value="SeachdownTest-分页列表查询", notes="SeachdownTest-分页列表查询")

@GetMapping(value = "/list")

public Result<?> queryPageList(SeachdownTest seachdownTest,

   @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,

   @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,

   HttpServletRequest req) {

QueryWrapper<SeachdownTest> queryWrapper = QueryGenerator.initQueryWrapper(seachdownTest, req.getParameterMap());

Page<SeachdownTest> page = new Page<SeachdownTest>(pageNo, pageSize);

IPage<SeachdownTest> pageList = seachdownTestService.page(page, queryWrapper);

return Result.OK(pageList);

}

重写覆盖的2个策略

第一个利用现在的QueryWrapper对象,获取多个表。然后循环处理。

第二个:是直接写Sql语句

自动生成的是直接采用了public interface SeachdownTestMapper extends BaseMapper<SeachdownTest>  ,泛型的方式。

但是不需要泛型。因为要关联其他的表

public interface SeachdownTestMapper extends BaseMapper

public interface UserMapper extends BaseMapper {

List findAll();

 List<User> selectByXml(@Param("name") String name);

}

编写SQL

<select id="selectByXml" resultType="com.example.mybatisplusdemo.sample.model.User">

    select *

    from user

    <where>

        <if test="name != null and name != ''">

            and name = #{name}

        </if>

    </where>

</select>

这样就一个Sql语句,就解决问题

Sql语句写在代码中,还是写在配置文件中?在配置文件中,是否可以进行编译检测,是否有什么好的代码工具?

  如果是后端管理系统

业务人员处理,没有变化

前端人员不要调整

后端人员

检查的页面,观察ER关系

页面中没有冗余信息的

在JPA的类中,调整FK的约束关系。重新生成代替,页面展现不变。

页面中有冗余信息的-字典id及所相关的字段

根据ER关系表,重新生成新的一套;之前业务人员创建的全部以mock作为前缀。

修改Controller

Insert:入口参数减少

Update,入口参数减少

Select,重新写xml的配置代码

2023年2月28日星期二

原则:

代码生成的不能修改,只能采用开闭原则,进行扩展开放

从页面配置到数据库,从数据库配置到IDEA的类双向有效。

页面代码生成第一次及后期生成的页面代码

只要是page开头的,全部覆盖。不保留历史版本。只能有代码生成工具覆盖它。不能修改。

只要是根据ER关系生成的开头是ER,全部覆盖,不保留历史版本。只能由代码生成工具覆盖它,不能修改。

自定义的代码

Controller 采用Page那一套代码的,但是加@Version标记

Service、Mapper的采用根据ER生成的一套的。

如果是复杂查询,那么Service、Mapper的还是采用Page的,但是数据库采用视图的方式来进行处理。

编写程序,本质是对数据进行编程

前端,对数据

后端,对数据

GIS,对数据

大数据分析,对数据

第一次

建表的时候,没有id,只有中文标签及英文字段

第二次

建表的时候,建立关系

第三次

个人写的时候,利用第一次建表的返回参数

JeecgController 解决的是excel的导入导出的父类

List转Page

框架中应用的,有以下几个必须要加载的类

返回整个树

增加List转Page的类,但是现在没有泛型化,应该可以泛型化

增加一个测试的类,对第一次,第二次调整的内容,进行测试。对数据库变更进行测试。结合测试的Mock数据来进行处理。

CodeFirst VS DBFirst

领域建模TDD

代码生成,一定要关注,能自动生成测试类。

答:自动生成的代码,不要进行测试,因为已经测试过了

DBFirst

DBFirst是一种数据库开发的方法,它是从数据库开始进行应用程序开发的过程。在DBFirst方法中,首先需要创建数据库模式(通常是关系型数据库),包括表、列、关系等。然后,根据数据库模式自动生成与之对应的实体类或数据访问层代码。

DBFirst方法的主要步骤如下:

创建数据库模式:使用数据库管理工具(如MySQL Workbench、Microsoft SQL Server Management Studio等)创建数据库模式,定义表、列、关系等数据库结构。

生成实体类或数据访问层代码:使用ORM(对象关系映射)工具或数据库工具,根据数据库模式自动生成与之对应的实体类或数据访问层代码。这些代码通常是使用编程语言(如C#、Java等)编写的,并包含了与数据库交互的方法和逻辑。

扩展生成的代码:根据业务需求,对生成的实体类或数据访问层代码进行扩展和定制,添加额外的业务逻辑、数据校验等。

进行应用程序开发:使用生成的实体类或数据访问层代码,开发应用程序的其他组件,如业务逻辑层、界面层等,以实现完整的应用功能。

DBFirst方法的优点是可以快速生成与数据库模式对应的代码,减少手动编写代码的工作量。同时,它与已有的数据库结构紧密耦合,可以直接使用数据库提供的功能和特性。然而,DBFirst方法也有一些局限性,例如在数据库模式变更时需要手动更新代码,不太适合频繁变动的数据库结构。

总的来说,DBFirst方法适用于以数据库为核心的应用开发,特别是在需要快速构建与数据库结构一致的数据访问层或实体类时。

DbFirst有的时候,如果已经有表,并且有数据。适合DbFirst的方式。采用update的方式。

如果DB的数据不断增长。进行关系化转换为数据库。在转过程中,比如有无法转的数据,把它纳入到新的问题跟踪的数据表中。

实体类上注解

常用在实体类上简化代码的注解

这些注解都是 Lombok 库提供的,用于简化 Java 代码中的 getter、setter、构造器等常见方法的编写。下面是每个注解的具体含义和用途:

@Data:

作用:这是一个组合注解,它相当于同时使用了 @ToString、@EqualsAndHashCode、@Getter on all fields、@Setter on all non-final fields 和 @RequiredArgsConstructor。

效果:自动生成所有字段的 getter 方法、非 final 字段的 setter 方法、toString 方法、equals 和 hashCode 方法以及一个包含所有必填字段的构造器。

有时候,为了更好的封装性,对某个字段的 getter 方法进行特殊处理,可以直接在类中声明该 getter 方法。Lombok 会优先使用你手动编写的 getter 方法而不是自动生成的那个。

 @Data

   public class Example {

       private String name;

       // 自定义 getter 方法

       public String getName() {

           return "Custom: " + name.toUpperCase();

       }

   }

@SuperBuilder:

作用:用于创建一个类的 builder 模式实现,并且允许子类继承这个 builder。这使得可以构建对象实例的同时支持可选参数和必填参数。

效果:除了生成 builder 类之外,还会为当前类生成所有属性的 getter 方法。如果类中定义了 @Builder 注解,则默认生成的 builder 不支持子类继承;使用 @SuperBuilder 可以解决这一问题,确保子类也能使用相同的 builder 模式。

@AllArgsConstructor:

作用:生成一个包含类中所有字段的构造器。

效果:适用于需要通过构造器初始化所有成员变量的情况,常用于不可变对象的设计。

@NoArgsConstructor:

作用:生成一个无参构造器。

效果:适用于需要默认构造器的情况,比如在使用某些框架(如 Spring)时,可能需要无参构造器来完成依赖注入等操作。

这些注解可以帮助开发者减少样板代码的编写,提高开发效率。根据具体需求选择合适的注解即可。

SPI机制

SPI 全称:Service Provider Interface

服务发现机制之前,

由总控的程序来进行配置化加载各个插件。

现在这个责任,被转移到 实现方。所以由一个服务发现的机制

SPI在JDBC中的应用

3.1 SPI机制在JDBC DriverManager中的应用

在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现

JDBC接口的定义

首先在java中只是定义了接口java.sql.Driver,具体的实现都是由不同厂商来提供的。

在mysql中的实现

在mysql的jar包
mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。

在postgresql中的实现

同样在postgresql的jar包postgresql-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。

JDBC的SPI机制

首先来个简单的代码示例

这里面,也会涉及到第一次加载完成之后的单例;采用静态变量的方式进行加载

命名规范

驼峰命名规范

驼峰命名(Camel Case)是一种命名约定,用于将多个单词组合在一起形成一个标识符(变量、方法、类名等)。

驼峰命名有两种常见的形式:

小驼峰命名(lower camel case):第一个单词的首字母小写,后续单词的首字母大写。例如:firstName、lastName、myVariable。

大驼峰命名(upper camel case):每个单词的首字母都采用大写形式。例如:FirstName、LastName、MyVariable。

驼峰命名的优点是增加了标识符的可读性,使其更易于理解和识别。它也是许多编程语言和约定的常用命名风格。

在编程中,驼峰命名通常用于变量名、方法名和类名等标识符的命名,以提高代码的可读性和一致性。以下是一些驼峰命名的示例:

// 变量名示例(小驼峰命名)

int studentId;

String firstName;

boolean isFlagEnabled;

// 方法名示例(小驼峰命名)

void calculateTotalScore();

String getFullName();

boolean isUserLoggedIn();

// 类名示例(大驼峰命名)

public class Person {

    // 类的成员变量(小驼峰命名)

    private String firstName;

    private String lastName;

    // 类的方法(小驼峰命名)

    public void setFirstName(String firstName) {

        this.firstName = firstName;

    }

    public void setLastName(String lastName) {

        this.lastName = lastName;

    }

    public String getFullName() {

        return firstName + " " + lastName;

    }

}

阿里巴巴的规范

Java开发手册提供了一套规范,其中包括了对驼峰命名的具体要求和建议。以下是阿里规范中关于驼峰命名的主要准则:

小驼峰命名(lower camel case)。

定义:首字母小写,后续每个单词的首字母大写

注意点:避免使用单个字符作为变量名,要有具体的描述性名称。

名称应具备见名知意的特点,能够清晰表达变量或方法的含义。

变量名应使用名词,方法名应使用动词。

示例:

firstName

lastName

employeeId

customerName

应用场景

一般应用与变量和方法的命名

大驼峰命名(upper camel case)。

定义:每个单词的首字母都大写。

注意点:类名应以名词或名词短语命名,避免使用缩写,除非是广泛被理解的缩写。

接口名应以形容词或形容词短语命名,表示该接口的特征或能力。

示例:

    public class EmployeeInformation {

        // 类的实现

    }

    public interface RemoteCallable {

        // 接口的定义

    }

应用场景

一般应用与类名和接口的命名

数据库表命名规范

全小写字母:

使用全小写字母,单词之间用下划线分隔。

示例:

employees

departments

orders

描述性名称:

表名应具有描述性,能够清晰表达表的内容。

示例:

employee_information

department_details

order_history

避免使用保留关键字:

避免使用 SQL 的保留关键字(如 select, from, where 等)作为表名。

统一前缀(可选):

可以为表名添加统一的前缀,便于管理和区分。

示例:

hr_employees

hr_departments

hr_orders

字段命名规范

全小写字母:

字段名也使用全小写字母,单词之间用下划线分隔。

示例:

employee_id

first_name

last_name

描述性名称:

字段名应具有描述性,能够清晰表达字段的含义。

示例:

employee_id

first_name

last_name

hire_date

salary

避免使用保留关键字:

避免使用 SQL 的保留关键字(如 select, from, where 等)作为字段名。

统一前缀(可选):

可以为字段名添加统一的前缀,便于管理和区分。

示例:

emp_id

emp_first_name

emp_last_name

使用复数形式(可选):

如果字段表示多个实体,可以使用复数形式。

示例:

phone_numbers

email_addresses

页面到业务层转换的规范

Web请求到java类。主要是靠json的转换。看json的规范

在 Jackson 中,可以通过配置 ObjectMapper 来指定 JSON 属性名称的命名策略。这些命名策略决定了如何将 Java 属性名称转换为 JSON 属性名称。以下是一些常见的命名策略及其配置方式:

常见的命名策略

SNAKE_CASE:所有字母均为小写,并在名称元素之间使用下划线作为分隔符,例如 snake_case。

UPPER_CAMEL_CASE:所有名称元素,包括第一个,都以大写字母开头,后跟小写字母,并且没有分隔符,例如 UpperCamelCase。

LOWER_CAMEL_CASE:所有名称元素,包括第一个,都以小写字母开头,后跟小写字母,并且没有分隔符,例如 lowerCamelCase。

LOWER_CASE:所有字母均为小写字母,没有分隔符,例如 lowercase。

KEBAB_CASE:名称元素之间用连字符分隔,例如 kebab-case。

LOWER_DOT_CASE:所有字母均为小写字母,用点连接字符,例如 lower.case。

业务到数据库层相互转换的规范

Mybatisplus的规范

采用驼峰规范,在插入的时候,根据java类的属性,上面的标注@TableField。如果不一样,可以通过这个注解来进行修改

在查询返回的时候,也是驼峰规范,如果不一样,可以在resultmap中进行一个个修改。

这样就有最大程度的灵活性。

Mybatisplus没有提供接口进行通用性的修改

Jpa的规范

插入的时候

@Column的方式,可以修改,默认的采用驼峰的方式

查询返回的时候,也是一样的,没有 插入差异。

SpringImplicitNamingStrategy ,SpringPhysicalNamingStrategy 2步骤来实现转换

WebHook

俗称钩子,在简道云中是可以由开发人员自定义的回调地址。

这是用户通过自定义回调函数的方式来改变 Web 应用的一种行为,这些回调函数可以由不是该 Web 应用官方的第三方用户或者开发人员来维护,修改。通过 Webhook,你可以自定义一些行为通知到指定的 URL 去。Webhook 的“自定义回调函数”通常是由一些事件触发的。用户通过配置,就可以使一个网站上的事件调用在另一个网站上表现出来,这些事件调用可以是任何事件,但通常应用的是系统集成和消息通知。

签名验证

为了防止 webhook 的接收服务器被第三方恶意攻击,用户在开发回调接口时,建议对回调请求进行签名校验,以确保回调请求来源来自于简道云。hook会以POST的形式将内容以JSON格式发送给指定地址。

http://b23925h769.qicp.vip/word2Eh/frontservice?nonce="ads"×tamp="2002-2-10"&token="asd"&payload="adad"

系统中

连接测试的地址

在实际传输的时候,只是取服务器的地址。抛弃掉它的参数

然后在简道云中组织参数

Nonce=457f01

Timestamp=1694159132

Token=asd

Payload=表单的格式数据

所以,在该次传输的时候,token是不变的,其余的都变化了

所以,猜测简道云中的逻辑

解析url

然后将nonce 参数,timestamp参数重新生成之后,进行替换重新组装url

软件的分层架构

根据业务架构实践,结合业界分层规范与流行技术框架分析,推荐分层结构如图所示,默认上层 依赖于下层,箭头关系表示可直接依赖,如:开放 API 层可以依赖于 Web 层(Controller 层),也可以 直接依赖于 Service 层,依此类推:

开放 API 层:可直接封装 Service 接口暴露成 RPC 接口;通过 Web 封装成 http 接口;网关控制层等。

终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等。

Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。

Service 层:相对具体的业务逻辑服务层。

Manager 层:通用业务处理层,它有如下特征

1)对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口。

2)对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。

3)与 DAO 层交互,对多个 DAO 的组合复用。

DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase、OceanBase等进行数据交互。

第三方服务:包括其它部门 RPC 服务接口,基础平台,其它公司的 HTTP 接口,如淘宝开放平台、支付宝付款服务、

高德地图服务等。

外部数据接口:外部(应用)数据存储服务提供的接口,多见于数据迁移场景中。

不变的部分作为框架的一个部分

有规律变化的部分采用模板生成

每个项目独立变化的部分代码进行编写

分层领域模型规约:

DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。

DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象

BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。

Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。

VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。

在不是很复杂的项目,经常采用只有VO,DO的处理方式。

数据类型的转换

在Java项目中,从界面到代码再返回到界面的过程中,涉及到的数据转换通常包括驼峰命名法(camelCase)与下划线命名法(snake_case)之间的转换。以下是几个常见的需要进行这种转换的地方:

Controller层:

在Controller层接收前端请求时,通常会将前端传来的参数转换为Java对象。

可以使用工具类或框架提供的功能进行自动转换。

第一层:

通过Jackson的转换

实现字符串json转为Java类的属性

第二层:

通过mybatis的框架进行转换

Java类到数据库的的表名及数据列

第三层:

通过mybtis的xml可以进行配置实现数据查询的列 与java类的映射

第四层

通过java类到页面的字符串的映射

如果通过JPA的时候,有XX层有差异

一般情况下,如果用户没有深入理解它默认的转换规则,可以采用全部小写的方式,在美观性及容错性方面做一个权衡,保证不用出错,放弃一下美观性的要求。

软件测试

单元测试在系统重构时能发挥巨大的做用,能够在重构后快速测试新的接口是否与重构前有出入。

为什么会越来越重视单元测试。

因为软件是不断迭代的,各种边界的条件,写在文档中,不如写在单元测试中。

如果没有单元测试,对软件的重构,有点不敢动手,每次都修改一点来执行软件重构。

在实际工作中,有些开发人员不喜欢写测试用例,感觉是在浪费时间,但是要知道,如果我们测试用例非常完备,是可以提升团队体效率的。那么这一讲我们就针对这一问题,来看看如何使用单元测试,以及如何快速地写单元测试。

 常见的有 junit 4、junit 5 两个版本,Spring Boot 2.2.0 版本开始引入 JUnit5 作为单元测试默认库引用的 junit5:    org.junit.jupiter.api ( junit4:    org.junit.)

有时候,要运行以前的参考代码,需要junit4的环境

Junit4:@RunWith(SpringRunner.class)  :作为启动器,它使得 JUnit 能够与 Spring 框架结合使用。这样,当运行测试时,Spring 的上下文会被加载,并且可以注入 Spring 管理的 bean  

Junit5:@ExtendWith 注解用于扩展测试类的功能。SpringExtension 是 Spring 提供的一个扩展点,它负责设置 Spring 的测试环境。这使得测试能够访问 Spring 上下文中的 bean,并支持 Spring 的依赖注入等功能,在SpringBootTest注解上有@ExtendWith(SpringExtension.class),在项目中,直接使用@SpringBootTest,它默认带有@ExtendWith(SpringExtension.class)

Spring Boot 提供了许多公用方法与注解,可以帮助开发者测试应用程序。Spring Boot 主要包括 spring-boot-test 与 spring-boot-test-autoconfigure 核心模块。Spring Boot 提供了 spring-boot-starter-test 的 Starter,主要集成了 JUnit Jupiter、AssertJ 和Hamcrest 等常用测试框架。

Spring Boot Test 简介

在 Spring Boot Test 诞生之前,常用的测试框架是 JUnit 等。Spring Boot Test 诞生后,集成了上述测试框架。Spring 框架的一个主要优势是更容易集成单元测试,使用 Spring Boot Test,需要在项目中增加 spring-boot-starter-test 的 Starter 依赖,

引入依赖

使用 Spring Boot Test,需要在项目中增加 spring-boot-starter-test 的 Starter 依赖,具体如下:

<dependency>

  <groupId>org.springframework.boot</groupId>

  <artifactId>spring-boot-starter-test</artifactId>

  <scope>test</scope>

</dependency>

使用 @SpringBootTest 注解,即可进行测试。

如果项目中依赖 spring-boot-starter-test,则自动添加以下类库。如表 1 所示:

通常情况下,Spring Boot Test 支持的测试种类可以分为以下 3 种:

单元测试:主要用于测试类功能等。

切片测试:介于单元测试与集成测试之间,在特定环境下才能执行。

集成测试:测试一个完整的功能逻辑。

SpringBootTest-Junit5的测试方法实例生命周期

为了隔离地执行单个测试方法,以及避免由于不稳定的测试实例状态引发非预期的副作用,JUnit会在执行每个测试方法执行之前创建一个新的实例。这个”per-method”测试实例生命周期是JUnit Jupiter的默认行为,这点类似于JUnit以前的所有版本。

如果希望JUnit Jupiter在同一个实例上执行所有的测试方法,在测试类上加上注解@TestInstance(Lifecycle.PER_CLASS)即可。启用了该模式后,每一个测试类只会创建一次实例。因此,如果测试方法依赖实例变量存储的状态,可能需要在@BeforeEach或@AfterEach方法中重置状态。

"per-class"模式相比于默认的"per-method"模式有一些额外的好处。具体来说,使用了"per-class"模式之后,你就可以在非静态方法和接口的default方法上声明@BeforeAll和 @AfterAll。因此,"per-class"模式使得在@Nested测试类中使用@BeforeAll和@AfterAll注解成为了可能。

为了避免复杂的配置,Spring 引入了大量的注解方式进行测试,这样可以减轻很多工作量

通用的注解

1. @SpringBootTest注解

使用 @SpringBootTest 的 WebEnvironment 属性来修改测试的运行方式。

MOCK:加载 Web 应用程序上下文并提供模拟的 Web 环境。该注解不会启动嵌入的服务器,可以结合@AutoConfigureMockMvc 和 @AutoConfigureWebTest-Client 注解使用。

RANDOM_PORT:加载 WebServerApplicationContext 并提供真实的 Web环境,嵌入的服务器启动后可以监听随机端口。

DEFINED_PORT:加载 WebServerApplicationContext 并提供真实的 Web 环境,嵌入的服务器启动后可以监听特定的端口。特定的端口可以从 application.properties 获取,也可以设置为默认的 8080 端口。

NONE:使用 SpringApplication 加载 ApplicationContext,但不提供任何 Web 环境。

@TestInstance

在Spring Boot项目中进行单元测试时,@TestInstance 注解可以用来控制测试类的实例化生命周期。这个注解有以下两个可选的生命周期模式:

PER_METHOD(默认):对于每个测试方法,都会创建一个新的测试类实例。

PER_CLASS:在整个测试类的所有测试方法执行期间,只创建一个测试类实例。

当你使用 @TestInstance(TestInstance.Lifecycle.PER_CLASS) 时,整个测试类的所有测试方法将共享同一个实例。这通常用于减少重复的初始化工作,比如数据库连接或复杂的对象图初始化。

下面是一个简单的Spring Boot测试类的例子,展示了如何使用 @TestInstance(TestInstance.Lifecycle.PER_CLASS):

2. @RunWith注解

Spring Boot Test 默认使用 JUnit 5 框架,@RunWith(SpringRunner.class) 注解可方便开发者使用 JUnit 4 框架。使用方式如下:

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

class DemoApplicationTests { ... }

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)

@Order: 用于配置方法的执行顺序,数字越低执行顺序越高。

@ActiveProfiles("test") // 使用特定的配置文件

@BeforeAll

 在测试类的所有测试方法前执行一次,可用于全局初始化。

@BeforeAll,这个注解的定义是使⽤了该注解的⽅法在当前整个测试类中所有的测试⽅法之前执⾏,每个测试类运⾏时只会执⾏⼀次。并且这个注解需在static⽅法上使⽤,有⼀种情况例外(声明了TestInstance.Lifecycle.PER_CLASS 情况下允许使⽤在⾮static⽅法上)

@AfterAll

 在测试类的所有测试方法后执行一次,可用于全局销毁资源。

@AfterAll,与@BeforeAll相对应,这个注解定义是在使⽤了该注解的⽅法在当前测试类中所有测试⽅法都执⾏完毕后执⾏的,每个测试类运⾏时只会执⾏⼀次。同样这个注解需要在static⽅法上使⽤,有⼀种例外情况。可以看到上⾯的输出结果AfterAll是最后⼀个输出的。

@BeforeEach

 在测试类的每个测试方法前都执行一次。

@BeforeEach ,这个注解表⽰了在每⼀个测试⽅法(测试⽅法表⽰为所有了@Test、@RepeatedTest 、@ParameterizedTest 或者@TestFactory注解的⽅法)之前执⾏。根据如上接⼝可以看到每次执⾏@Test⽅法之前

都会先执⾏@BeforeEach 注解的⽅法⼀次(此⽅法执⾏输出结果为BeforeEach )。注意不要与@BeforeAll概念混淆,@BeforeEach 会每⼀个测试⽅法执⾏之前都会执⾏,@BeforeAll只会执⾏⼀次。

@AfterEach

在测试类的每个测试方法后都执行一次。

@Test

表⽰该⽅法是⼀个测试⽅法。

JUnit 4的@Test需要导入的import org.junit.Test;

Junit5的@Test需要导入的import org.junit.jupiter.api.Test;

注解不同的是,它没有声明任何属性,因为JUnit Jupiter中的测试扩展是基于它们⾃⼰的专⽤注解来完成的。

@DisplayName

,该注解为测试类或测试⽅法声明了⼀个⾃定义显⽰的名称,这个是什么意思呢,可以看如下执⾏结果表,

@RepeatedTest

: 指定测试方法重复执行。

@ParameterizedTest

: 指定参数化测试方法,类似重复执行,从@ValueSource中获取参数。

@ValueSource

: 用于参数化测试指定参数。

@AutoConfigureMockMvc

: 启用MockMvc的自动配置,可用于测试接口。

Spring Boot - 用JUnit 5构建完美的Spring Boot测试套件_junt5 模拟springboot环境-CSDN博客

断言

断言是用来验证测试结果是否符合预期的主要工具。JUnit 5 提供了一个丰富的断言API,这些断言方法位于 org.junit.jupiter.api.Assertions 类中。下面是一些常用的断言方法及其用法示例。

   @Test

    void testAdd() {

        // 测试两个正数相加

        Assertions.assertEquals(5, calculator.add(2, 3), "2 + 3 应该等于 5");

        // 测试负数与正数相加

        assertEquals(-1, calculator.add(-3, 2), "-3 + 2 应该等于 -1");

        // 测试两个负数相加

        assertEquals(-6, calculator.add(-3, -3), "-3 + (-3) 应该等于 -6");

        // 测试零与其他数字相加

        assertEquals(3, calculator.add(0, 3), "0 + 3 应该等于 3");

        // 测试两个零相加

        assertEquals(0, calculator.add(0, 0), "0 + 0 应该等于 0");

    }

    @Test

    void testAddWithSoftAssertions() {

        var sa = assertAll("add method",

            () -> assertEquals(5, calculator.add(2, 3), "2 + 3 应该等于 5"),

            () -> assertEquals(-1, calculator.add(-3, 2), "-3 + 2 应该等于 -1"),

            () -> assertEquals(-6, calculator.add(-3, -3), "-3 + (-3) 应该等于 -6")

        );

    }

    @Test

    void testAddWithAssumptions() {

        assumeTrue(System.getProperty("os.name").startsWith("Windows"), "This test only runs on Windows");

        // 测试仅在假设成立的情况下进行

        assertEquals(5, calculator.add(2, 3), "2 + 3 应该等于 5");

    }

    @Test

    void testAddWithNull() {

        // 假设 add 方法不允许传入 null

        assertThrows(NullPointerException.class, () -> calculator.add(null, 3), "传递 null 参数应抛出 NullPointerException");

    }

}

数据层测试

Mybatisplus的数据层测试

添加测试依赖

<dependency>

    <groupId>com.baomidou</groupId>

    <artifactId>mybatis-plus-boot-starter-test</artifactId>

    <version>3.5.7</version>

</dependency>

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import static org.assertj.core.api.Assertions.assertThat;

@MybatisPlusTest

class MybatisPlusSampleTest {

    @Autowired

    private SampleMapper sampleMapper;

    @Test

    void testInsert() {

        Sample sample = new Sample();

        sampleMapper.insert(sample);

        assertThat(sample.getId()).isNotNull();

    }

}

@DataJpaTest 注解

@DataJpaTest 注解可以测试 JPA 应用。默认情况下,该注解会扫描 @Entity 注解的类及 repositories 类。示例代码如下:

import org.junit.jupiter.api.Test;

import org.springframework.boot.test.autoconfigure.orm.jpa.*;

import static org.assertj.core.api.Assertions.*;

//JPA测试

@DataJpaTest

class ExampleRepositoryTests {

    @Autowired

    private TestEntityManager entityManager;

    @Autowired

    private UserRepository repository;

    @Test

    void testExample() throws Exception {

        this.entityManager.persist(new User("sboot", "1234"));

        User user = this.repository.findByUsername("sboot"); 

        assertThat(user.getUsername()).isEqualTo("sboot");

        //断言用户名

        assertThat(user.getVin()).isEqualTo("1234");

    }

}

在使用@DataJpaTest的时候,如果要使用test中的数据库,需要在test的resource目录下,

测试的时候,如果不需要使用默认的数据库h2,那么需要在@DataJpaTest增加

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

如果不需要回滚,那么在@Test上增加这个选项

@Rollback(false)

如果在test中,需要先执行sql语句,那么在test的resouce下设置2个文件,就可以正确的运行。

@Sql(scripts = {"classpath:schema.sql", "classpath:data.sql"})

例2

假设我们项目里面有 Address 和 AddressRepository,代码如下所示。

@Entity

@Table

@Data

@SuperBuilder

@AllArgsConstructor

@NoArgsConstructor

public class Address extends BaseEntity {

   private String city;

   private String address;

}

//Repository的DAO层

public interface AddressRepository extends JpaRepository<Address, Long>{

  

}

第三步:新建 AddressRepositoryTest 

@DataJpaTest

public class AddressRepositoryTest {

    @Autowired

    private AddressRepository addressRepository;

    //测试一下保存和查询

    @Test

    public  void testSave() {

        Address address = Address.builder().city("shanghai").build();

        addressRepository.save(address);

        List<Address> address1 = addressRepository.findAll();

        address1.stream().forEach(address2 -> System.out.println(address2));

    }

}

通过上面的测试用例可以看到,我们直接添加了 @DataJpaTest 注解,然后利用 Spring 的注解 @Autowired,引入了 spring context 里面管理的 AddressRepository 实例。在这里面使用了集成测试,即直接连接的数据库来完成操作。

直接运行上面的测试用例,可以得到如下图所示的结果。

通过测试结果,我们可以发现:

我们的测试方法默认都会开启一个事务,测试完了之后就会进行回滚;

里面执行了 insert 和 select 两种操作;

如果开启了 Session Metrics 的日志的话,也可以观察出来其发生了一次 connection。

通过这个案例,我们可以知道 Repository 的测试用例写起来还是比较简单的,其中主要利用了 @DataJpaTest 的注解。下面我们打开 @DataJpaTest 的源码,看一下。

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Inherited

@BootstrapWith(DataJpaTestContextBootstrapper.class) //测试环境的启动方式

@ExtendWith(SpringExtension.class)//加载了Spring测试环境

@OverrideAutoConfiguration(enabled = false)

@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)

@Transactional

@AutoConfigureCache

@AutoConfigureDataJpa//加载了依赖Spring Data JPA的原有配置

@AutoConfigureTestDatabase //加载默认的测试数据库,我们这里面采用默认的H2

@AutoConfigureTestEntityManager//加载测试所需要的EntityManager,主要是事务处理机制不一样

@ImportAutoConfiguration

public @interface DataJpaTest {

   //默认打开sql的控制台输出,所以当我们什么都没有做的时候就可以看到SQL了

   @PropertyMapping("spring.jpa.show-sql")

   boolean showSql() default true;

......}

通过源码会发现 @DataJpaTest 注解帮我们做了很多事情:

加载 Spring Data JPA 所需要的上下文,即数据库,所有的 Repository;

启用默认集成数据库 h2,完成集成测试。

可能在工作中,有的同事会说没有必要写 Repository 的测试用例,因为好多方法都是框架里面提供的,况且这个东西没有什么逻辑,写的时候有点浪费时间。

其实不然,如果能把 Repository 的测试用写好的话,这对我们的开发效率绝对是有提高的。否则当给你一个项目,让你直接改里面的代码,你可能就会比较慌,不敢改。所有你就要知道都有哪些场景我们

现在我们知道了 @DataJpaTest 所具备的能力,那么在实际工作中,哪些场景会需要写 Repository 的测试用例呢?

必须要写 Repository 的测试用例。

场景一:当新增一个 Entity 和实体对应的 Repository 的时候,需要写个简单的 save 和查询测试用例,主要目的是检查我们的实体配置是否正确,否则当你写了一大堆 Repository 和 Entity 的时候,启动报错,你就傻眼了,不知道哪里配置得有问题,这样反而会降低我们的开发效率;

场景二:当实体里面有一些 POJO 的逻辑,或者某些字段必须要有的时候,我们就需要写一些测试用例,假设我们的 Address 实体里面不需要有 address 属性字段,并且有一个 @Transient 的字段和计算逻辑,如下述代码所示。

public class Address extends BaseEntity {

   @JsonProperty("myCity")

   private String city;

   private String address; //必要字段

   @Transient //非数据库字段,有一些简单运算

   private String addressAndCity;

   public String getAddressAndCity() {

      return address+"一些简单逻辑"+city;

   }

}

这时我们就需要写一些测试用例去验证一下了。
场景三:当我们有自定义的方法的时候,就可能需要测试一下,看看返回结果是否满足需求,代码如下所示。

public interface AddressRepository extends JpaRepository<Address, Long>{

    Page<Address> findByAddress(@Param("address") String address, Pageable pageable);

}

场景四:当我们利用 @Query注解,写了一些 JPQL 或者 SQL 的时候,就需要写一次测试用例来验证一下,代码如下所示。

public interface AddressRepository extends JpaRepository<Address, Long>{

    //通过@Query注解自定的JPQL或Navicat SQL

    @Query(value = "FROM Address where deleted=false ")

    Page<Address> findAll(Pageable pageable);

}

那么对应的复杂一点的测试用例就要变成如下面这段代码所示的样子。

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

@DataJpaTest

public class AddressRepositoryTest {

    @Autowired

    private AddressRepository addressRepository;

    @BeforeAll //利用 @BeforeAll准备一些Repositroy需要的测数据

    @Rollback(false)// 由于每个方法都是有事务回滚机制的,为了测试我们的Repository可能需要模拟一些数据,所以我们改变回滚机制

    @Transactional

    public void init() {

        Address address = Address.builder().city("shanghaiDeleted").deleted(true).build();

        addressRepository.save(address);

    }

    //测试没有包含删除的记录

    @Test

    public  void testFindAllNoDeleted() {

        List<Address> address1 = addressRepository.findAll();

        int deleteSize = address1.stream().filter(d->d.equals("shanghaiDeleted")).collect(Collectors.toList()).size();

        Assertions.assertTrue(deleteSize==0); //测试一下不包含删除的条数

    }

}

场景五:当我们测试一些 JPA 或者 Hibernate 的底层特性的时候,测试用例可以很好地帮助我们。因为如果依赖项目启动来做测试,效率太低了,例如我们之前讲的一些 @PersistenceContext 特性,那么就可以通过类似如下的测试用例完成测试。

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

@DataJpaTest

@Import(TestConfiguration.class)

public class UserInfoRepositoryTest {

    @Autowired

    private UserInfoRepository userInfoRepository;

    //测试一些手动flush的机制

    @PersistenceContext

            (properties = {@PersistenceProperty(

                    name = "org.hibernate.flushMode",

                    value = "MANUAL"//手动flush

            )})

    private EntityManager entityManager;

    @BeforeAll

    @Rollback(false)

    @Transactional

    public void init() {

        //提前准备一些数据方便我们测试

        UserInfo u1 = UserInfo.builder().id(1L).lastName("jack").version(1).build();

        userInfoRepository.save(u1);

    }

    @Test

    @Transactional

    public void testLife() {

        UserInfo userInfo = UserInfo.builder().name("new name").build();

        //新增一个对象userInfo交给PersistenceContext管理,即一级缓存

        entityManager.persist(userInfo);

        //此时没有detach和clear之前,flush的时候还会产生更新SQL

        userInfo.setName("old name");

        entityManager.flush();

        entityManager.clear();

//        entityManager.detach(userInfo);

        // entityManager已经clear,此时已经不会对UserInfo进行更新了

        userInfo.setName("new name 11");

        entityManager.flush();

        //由于有cache机制,相同的对象查询只会触发一次查询SQL

        UserInfo u1 = userInfoRepository.findById(1L).get();

        //to do some thing

        UserInfo u2 = userInfoRepository.findById(1L).get();

    }

}

测试场景可能远不止我上面举例的这些,总之你要灵活地利用测试用例来判断某些方法或者配置是否达成预期效果还是挺方便的。其中初始化数据的方法也有很多,我也只是举一个例子,期望你可以举一反三。

以上我们利用 @DataJpaTest 帮我们完成了数据层的集成测试,但是实际工作中,我们也会用到纯粹的单元测试,那么集成测试和单元测区别是什么?我们必须要搞清楚。

Tips:

在IDEA中,将光标放置在类的名字上面,按快捷键Ctrl+Shift+T创建测试类,此时生成的测试类在test文件夹里面。

测试的时候,在测试的类上要增加这个配置,否则它永远执行 默认的h2数据库,即使你在yml中仅仅配置mysql没有配置h2

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)//如果没有这句话,就是默认的h2

5. @DataMongoTest注解

@DataMongoTest 注解可以用来测试 MongoDB 程序。默认会配置一个嵌入的 MongoDB 并配置一个 MongoTemplate 对象,然后扫描 @Document 注解类。示例代码如下:

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;

import org.springframework.data.mongodb.core.MongoTemplate;

//Mongo测试

@DataMongoTest

class ExampleDataMongoTests {

    @Autowired

    private MongoTemplate mongoTemplate;

    ...

}

6. @DataRedisTest注解

@DataRedisTest 注解用来测试 Redis 应用程序。示例代码如下:

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest;

//Redis测试

@DataRedisTest

class ExampleDataRedisTests {

    @Autowired

    private YourRepository repository;

    ...

}

Service层测试

Service 层单元测试

首先,我们模拟一个业务中的 Service 方法,代码如下所示。

@Component

public class UserInfoServiceImpl implements UserInfoService {

   @Autowired

   private UserInfoRepository userInfoRepository;

   //假设有个 findByUserId的方法经过一些业务逻辑计算返回了一个业务对象UserInfoDto

   @Override

   public UserInfoDto findByUserId(Long userId) {

      UserInfo userInfo = userInfoRepository.findById(userId).orElse(new UserInfo());

      //模拟一些业务计算改变一下name的值返回

      UserInfoDto userInfoDto = UserInfoDto.builder().name(userInfo.getName()+"_HELLO").id(userInfo.getId()).build();

      return userInfoDto;

   }

}

其次,service 通过 Spring 的 @Component 注解进行加载,UserInfoRepository 通过 spring 的 @Autowired 注入进来,我们来测试一下 findByUserId 这个业务 service 方法,单元测试写法如下。

复制代码

@ExtendWith(SpringExtension.class)//通过这个注解利用Spring的容器

@Import(UserInfoServiceImpl.class)//导入要测试的UserInfoServiceImpl

public class UserInfoServiceTest {

    @Autowired //利用spring的容器,导入要测试的UserInfoService

    private UserInfoService userInfoService;

    @MockBean //里面@MockBean模拟我们service中用到的userInfoRepository,这样避免真实请求数据库

    private UserInfoRepository userInfoRepository;

    // 利用单元测试的思想,mock userInfoService里面的UserInfoRepository,这样Service层就不用连接数据库,就可以测试自己的业务逻辑了

    @Test

    public void testGetUserInfoDto() {

//利用Mockito模拟当调用findById(1)的时候,返回模拟数据

                Mockito.when(userInfoRepository.findById(1L)).thenReturn(java.util.Optional.ofNullable(UserInfo.builder().name("jack").id(1L).build()));

        UserInfoDto userInfoDto = userInfoService.findByUserId(1L);

        //经过一些service里面的逻辑计算,我们验证一下返回结果是否正确

        Assertions.assertEquals("jack",userInfoDto.getName());

    }

}

这样就可以完成了 Service 层的测试了。

其中 @ExtendWith(SpringExtension.class) 是 spring boot 与 Junit 5 结合使用的时候,当利用 Spring 的 TesatContext 进行 mock 测试时要使用的。有的时候如果们做一些简单 Util 的测试,就不一定会用到 SpringExtension.class。

在 service 的单元测试中,主要用到的知识点有四个。

通过 @ExtendWith(SpringExtension.class) 加载 Spring 的测试框架及其 TestContext;

通过 @Import(UserInfoServiceImpl.class) 导入具体要测试的类,这样 SpringTestContext 就不用加载项目里面的所有类,只需要加载 UserInfoServiceImpl.class 就可以了,这样可以大大提高测试用例的执行速度;

通过 @MockBean 模拟 UserInfoSerceImpl 依赖的 userInfoRepository,并且自动注入 Spring test context 里面,这样 Service 里面就自动有依赖了;

利用 Mockito.when().thenReturn() 的机制,模拟测试方法。

这样我们就可以通过 Assertions 里面的断言来测试 serice 方法里面的逻辑是否符合预期了。那么接下来我们看看 Controller 层的测试用例要怎么写。

WebController层测试

我们新增一个 UserInfoController 跟进 Id 获得 UserInfoDto 的信息,代码如下所示。

@RestController

public class UserInfoController {

   @Autowired

   private UserInfoService userInfoService;

   //跟进UserId取用户的详细信息

   @GetMapping("/user/{userId}")

   public UserInfoDto findByUserId(@PathVariable Long userId) {

      return userInfoService.findByUserId(userId);

   }

}

那么我们看一下 Controller 里面完整的测试用例,代码如下所示。

package com.example.jpa.demo;

import com.example.jpa.demo.service.UserInfoService;

import com.example.jpa.demo.service.dto.UserInfoDto;

import com.example.jpa.demo.web.UserInfoController;

import org.junit.jupiter.api.Test;

import org.mockito.Mockito;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;

import org.springframework.boot.test.mock.mockito.MockBean;

import org.springframework.http.MediaType;

import org.springframework.mock.web.MockHttpServletResponse;

import org.springframework.test.web.servlet.MockMvc;

import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserInfoController.class)

public class UserInfoControllerTest {

    @Autowired

    private MockMvc mvc;

    @MockBean

    private UserInfoService userInfoService;

    

    //单元测试mvc的controller的方法

    @Test

    public void testGetUserDto() throws Exception {

        //利用@MockBean,当调用 userInfoService的findByUserId(1)的时候返回一个模拟的UserInfoDto数据

        Mockito.when(userInfoService.findByUserId(1L)).thenReturn(UserInfoDto.builder().name("jack").id(1L).build());

        

        //利用mvc验证一下Controller里面的解决是否OK

        MockHttpServletResponse response = mvc

                .perform(MockMvcRequestBuilders

                        .get("/user/1/")//请求的path

                        .accept(MediaType.APPLICATION_JSON)//请求的mediaType,这里面可以加上各种我们需要的Header

                )

                .andDo(print())//打印一下

                .andExpect(status().isOk())

                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("jack"))

                .andReturn().getResponse();

        System.out.println(response);

    }

}

其中我们主要利用了 @WebMvcTest 注解,来引入我们要测试的 Controller。打开 @WebMvcTest 可以看到关键源码,如下图所示。

我们可以看得出来,@WebMvcTest 帮我们加载了 @ExtendWith(SpringExtension.class),所以不需要额外指定,就拥有了 Spring 的 test context,并且也自动加载了 mvc 所需要的上下文 WebMvctestContextbootstrapper。

有的时候可能有一些全局的 Filter,也可以通过此注解里面的 includeFilters 和 excluedeFilters 加载和排除我们需要的 WebMvcFilter 进行测试。

当通过 @WebMvcTest(UserInfoController.class) 导入我们需要测试的 Controller 之后,就可以再通过 MockMvc 请求到我们加载的 Contoller 里面的 path 了,并且可以通过 MockMvc 提供的一些方法发送请求,验证 Controller 的响应结果。

下面概括一下 Contoller 层单元测试主要用到的三个知识点。

利用 @WebMvcTest 注解,加载我们要测试的 Controller,同时生成 mvc 所需要的 Test Context;

利用 @MockBean 默认 Controller 里面的依赖,如 Service,并通过 Mockito.when().thenReturn();的语法 mock 依赖的测试数据;

利用 MockMvc 中提供的方法,发送 Controller 的 Rest 风格的请求,并验证返回结果和状态码。

什么是单元测试

通俗来讲,就是不依赖本类之外的任何方法完成本类里面的所有方法的测试,也就是我们常说的依赖本类之外的,都通过 Mock 的方式进行。那么在单元测试的模式下,我们一起看看 Controller层的单元测试应该怎么写。

Service 及Dao的测试依赖与持久化框架。放在不同的持久化框架中来写。

我们新增一个 UserInfoController 跟进 Id 获得 UserInfoDto 的信息,代码如下所示。

@RestController

public class UserInfoController {

   @Autowired

   private UserInfoService userInfoService;

   //跟进UserId取用户的详细信息

   @GetMapping("/user/{userId}")

   public UserInfoDto findByUserId(@PathVariable Long userId) {

      return userInfoService.findByUserId(userId);

   }

}

那么我们看一下 Controller 里面完整的测试用例,代码如下所示。

package com.example.jpa.demo;

import com.example.jpa.demo.service.UserInfoService;

import com.example.jpa.demo.service.dto.UserInfoDto;

import com.example.jpa.demo.web.UserInfoController;

import org.junit.jupiter.api.Test;

import org.mockito.Mockito;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;

import org.springframework.boot.test.mock.mockito.MockBean;

import org.springframework.http.MediaType;

import org.springframework.mock.web.MockHttpServletResponse;

import org.springframework.test.web.servlet.MockMvc;

import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserInfoController.class)

public class UserInfoControllerTest {

    @Autowired

    private MockMvc mvc;

    @MockBean

    private UserInfoService userInfoService;

    //单元测试mvc的controller的方法

    @Test

    public void testGetUserDto() throws Exception {

        //利用@MockBean,当调用 userInfoService的findByUserId(1)的时候返回一个模拟的UserInfoDto数据

        Mockito.when(userInfoService.findByUserId(1L)).thenReturn(UserInfoDto.builder().name("jack").id(1L).build());

        //利用mvc验证一下Controller里面的解决是否OK

        MockHttpServletResponse response = mvc

                .perform(MockMvcRequestBuilders

                        .get("/user/1/")//请求的path

                        .accept(MediaType.APPLICATION_JSON)//请求的mediaType,这里面可以加上各种我们需要的Header

                )

                .andDo(print())//打印一下

                .andExpect(status().isOk())

                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("jack"))

                .andReturn().getResponse();

        System.out.println(response);

    }

}

其中我们主要利用了 @WebMvcTest 注解,来引入我们要测试的 Controller。打开 @WebMvcTest 可以看到关键源码,如下图所示。

我们可以看得出来,@WebMvcTest 帮我们加载了 @ExtendWith(SpringExtension.class),所以不需要额外指定,就拥有了 Spring 的 test context,并且也自动加载了 mvc 所需要的上下文 WebMvctestContextbootstrapper。

有的时候可能有一些全局的 Filter,也可以通过此注解里面的 includeFilters 和 excluedeFilters 加载和排除我们需要的 WebMvcFilter 进行测试。

当通过 @WebMvcTest(UserInfoController.class) 导入我们需要测试的 Controller 之后,就可以再通过 MockMvc 请求到我们加载的 Contoller 里面的 path 了,并且可以通过 MockMvc 提供的一些方法发送请求,验证 Controller 的响应结果。

下面概括一下 Contoller 层单元测试主要用到的三个知识点。

利用 @WebMvcTest 注解,加载我们要测试的 Controller,同时生成 mvc 所需要的 Test Context;

利用 @MockBean 默认 Controller 里面的依赖,如 Service,并通过 Mockito.when().thenReturn();的语法 mock 依赖的测试数据;

利用 MockMvc 中提供的方法,发送 Controller 的 Rest 风格的请求,并验证返回结果和状态码。

3. @WebMvcTest注解

如果要测试 Spring MVC controllers 是否按预期那样工作,则用 @WebMvcTest 注解。@WebMvcTest 注解可自动配置 Spring MVC,并会限制扫描 @Controller 和 @ControllerAdvice 等注解的 Bean。

通常,@WebMvcTest 仅限于单个 Controller,并结合 @MockBean 注解提供对某个类的模拟实现。@WebMvcTest 还会自动配置MockMvc。MockMvc 提供了一个强大的方法可以快速测试 MVC 控制器,并且无须启动一个完整的 HTTP 服务器。示例代码如下:

@WebMvcTest(UserVehicleController.class)

class MyControllerTests {

    @Autowired

    private MockMvc mvc;   //注入MockMvc

    @MockBean

    private UserVehicleService userVehicleService;

    @Test

    void testExample() throws Exception {

        given(this.userVehicleService.getVehicleDetails("sboot")).willReturn(new VehicleDetails("Honda", "Civic"));

        this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN)).andExpect(status().isOk()).andExpect(content().string("Honda Civic"));

    }

}

7. @RestClientTest注解

@RestClientTest 注解用来测试 REST clients。默认情况下会自动配置 Jackson、GSON、Jsonb、RestTemplateBuilder,以及对MockRestServiceServer 的支持。示例代码如下:

@RestClientTest(RemoteVehicleDetailsService.class)

class ExampleRestClientTest {

    @Autowired

    private RemoteVehicleDetailsService service;

    @Autowired

    private MockRestServiceServer server;

    @Test

    void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails()

            throws Exception {

        this.server.expect(requestTo("/greet/details")).andRespond(withSuccess("hello", MediaType.TEXT_PLAIN));

        String greeting = this.service.callRestService();

        assertThat(greeting).isEqualTo("hello");

    }

}

8. @AutoConfigureMockMvc注解

@SpringBootTest 注解通常不会启动服务器,如果在测试中用 Web 端点进行测试,可以添加 MockMvc 配置。示例代码如下:

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest

@AutoConfigureMockMvc

class MockMvcExampleTests {

    @Test

    void exampleTest(@Autowired MockMvc mvc) throws Exception {

        mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World"));

    }

}

9. @MockBean注解

测试的过程中某些场景需要模拟一些组件,这时就需要使用 @MockBean 注解。示例代码如下:

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.*;

import org.springframework.boot.test.context.*;

import org.springframework.boot.test.mock.mockito.*;

import static org.assertj.core.api.Assertions.*;

import static org.mockito.BDDMockito.*;

@SpringBootTest

class MyTests {

    @MockBean

    private RemoteService remoteService;

    @Autowired

    private Reverser reverser;

    @Test

    void exampleTest() {

        given(this.remoteService.someCall()).willReturn("mock");

        String reverse = reverser.reverseSomeCall();

        assertThat(reverse).isEqualTo("kcom");

    }

}

集成测试

顾名思义,就是指多个模块放在一起测试,和单元测试正好相反,并非采用 mock 的方式测试,而是通过直接调用的方式进行测试。也就是说我们依赖 spring 容器进行开发,所有的类之间直接调用,模拟应用真实启动时候的状态。我们先从 Service 层进行了解。

Service层的集成测试

还用刚才的例子,看一下 UserInfoService 里面的 findByUserId 通过集成测试如何进行。测试用例的写法如下。

@DataJpaTest

@ComponentScan(basePackageClasses= UserInfoServiceImpl.class)

public class UserInfoServiceIntegrationTest {

    @Autowired

    private UserInfoService userInfoService;

    @Autowired

    private UserInfoRepository userInfoRepository;

    @Test

    @Rollback(false)//如果我们事务回滚设置成false的话,数据库可以真实看到这条数据

    public void testIntegtation() {

        UserInfo u1 = UserInfo.builder().name("jack-db").ages(20).id(1L).telephone("1233456").build();

        //数据库真实加一条数据

        userInfoRepository.save(u1);//数据库里面真实保存一条数据

        UserInfoDto userInfoDto =  userInfoService.findByUserId(1L);

        userInfoDto.getName();

        Assertions.assertEquals(userInfoDto.getName(),u1.getName()+"_HELLO");

    }

}

我们执行一下测试用例,结果如下图所示。

这时你会发现数据已经不再回滚,也会正常地执行 SQL,而不是通过 Mock 的方式测试。Service 的集成测试相对来说还比较简单,那么我们看下 Controller 层的集成测试用例应该怎么写。

Controller 层的集成测试用例的写法

Controller层的测试就要采用SpringBootTest,整个应用进行启动

我们用集成测试把刚才 UserInfoCotroller 写的 user/1/ 接口测试一下,将集成测试的代码做如下改动。

@SpringBootTest(classes = DemoApplication.class,

        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) //加载DemoApplication,指定一个随机端口

public class UserInfoControllerIntegrationTest {

    @LocalServerPort //获得模拟的随机端口

    private int port;

    @Autowired //我们利用RestTemplate,发送一个请求

    private TestRestTemplate restTemplate;

    @Test

    public void testAllUserDtoIntegration() {

        UserInfoDto userInfoDto = this.restTemplate

                .getForObject("http://localhost:" + port + "/user/1", UserInfoDto.class);//真实请求有一个后台的API

        Assertions.assertNotNull(userInfoDto);

    }

}

@SpringBootTest,它用来指定 Spring 应用的类是哪个,也就是我们真实项目的 Application 启动类;然后会指定一个端口,此处必须使用随机端口,否则可能会有冲突(如果我们启动的集成测试有点多的情况)。

再看日志的话,会发现此次的测试用例会在内部启动一个 tomcat 容器,然后再利用 TestResTemplate 进行真实请求,返回测试结果进行测试。

日志如下图所示。

如果我们看 @SprintBootTest 源码的话,会发现这个注解也是加载了 Spring 的测试环境 SpringExtension.class,并且里面有很多属性可以设置,测试的时候的配置文件 properties 和一些启动的环境变量 WebEnv;然后我们又利用了 Spring Boot Test 提供的 @LocalServerPort 获得启动时候的端口。源码如下图所示。

虽然集成测试用法也是比较简单的,甚至可能比 Mock 的测试环境更简单,因为集成测试可以取到 Application 启动之后加载的任何 Bean。但是实际工作中我们使用集成测试的时候,还是需要思考一些问题。

集成测试的一些思考

1.所有的方法都需要集成测试吗?

这是我们写集成测试用的时候需要思考的,因为集成测试用例需要内部启动 Tomcat 容器,所以可能会启动得慢一点。如果我们的项目加载的配置文件越来越多,势必会导致测试也会变慢。假设我们测试一个简单的逻辑就需要启动整个 Application,那么显然是不妥的。

那么我们整个 Application 不需要集成测试吗?也显然不是的,因为有些时候只有集成在一起才会发生问题,最简单的一个集成测试是我们需要测试是否能正常启动,所以一个项目里面会有个 ApplicationTests 来测试项目是否能正常启动。代码如下所示。

复制代码

@SpringBootTest

class DemoApplicationTests {

//测试项目是否能正常启动

   @Test

   void contextLoads() {

   }

}

2.一定是非集成测试就是单元测试吗?

实际工作中并没有划分那么清楚,有的时候我们集成了 N 个组件一起测试,可能就是不连数据库。比如我们可能会使用 Feign-Client 根据第三方的接口获取一些数据,那么我们正常的做法就是新建一个 Service,代码如下所示。

/**

 * 测试普通JSON返回结果,根据第三方接口取一个数据

 */

@FeignClient(name = "aocFeignTest", url = "http://room-api.staging.jack.net")

interface AppSettingService {

    @GetMapping("/api/v1/app/globalSettings")

    HashMap<String,Object> getAppSettings();

}

那么这个时候如果我们要测试,显然不需要启动整个 Application 来完成,但是需要按需加载一些 Configration 才能测试,那么测试用例会变成如下情况。

复制代码

@ExtendWith(SpringExtension.class)//利用Spring上下文

@Import({FeignSimpleConfiguration.class, FeignAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class})//导入此处Fegin-Client测试所需要的配置文件

@EnableFeignClients(clients = AppSettingService.class)//通过FeignClient的注解加载AppSettingService客户端。

/**

 * 依赖HTTPMessageConverter的使用方法(import FeignSimpleConfiguration junit)

 */

public class FeignJsonTest {

    @Autowired利用Spring的上下文注入appSettingService

    private AppSettingService appSettingService;

    @Test

    public void testJsonFeignClient() {

        HashMap<String,Object>  r = .getAppSettings();

        Assert.assertNotNull(r.get("data"));//测试一下接口返回的结果

    }

}

你会发现这个时候其实并没有启动这个 Application,但是我们也集成了 Fegin-Client 所需要的上下文 Configuration,从而利用 SpringExtension 加载所需要依赖的类,完成一次测试。

所以你一定要理解单元测试和集成测试的本质,根据自己的实际需要选择性地加载一些类来完成测试用例,而不是每次测试的时候都需要把所有类都加载一遍,这样返回会使测试用例的时间变长,从而降低工作效率。

在应用层测试时候,有几种方法的差别

MockMvc resttemplate feign 在web进行单元测试的时候区别

在进行 Web 单元测试时,MockMvc、RestTemplate 和 Feign 都可以用来模拟或实际执行 HTTP 请求,但它们在使用方式和适用场景上有一些区别。下面我将为您解释它们在 Web 单元测试中的区别和用法:

MockMvc:

作用:MockMvc 是 Spring Framework 提供的用于测试 Spring MVC 控制器行为的框架。它模拟了 Spring MVC 的运行环境,可以发送模拟的 HTTP 请求,测试控制器的请求映射、请求参数处理和返回结果。

使用场景:适用于测试控制器的单元测试,关注于测试 MVC 层的逻辑,而不需要启动完整的服务器。

示例用法:

java

mockMvc.perform(get("/api/someEndpoint"))

       .andExpect(status().isOk())

       .andExpect(content().json(expectedJson));

RestTemplate:

作用:RestTemplate 是 Spring Framework 提供的用于发送 HTTP 请求的客户端库。它实际发起 HTTP 请求,可以与外部服务进行通信,获取数据和进行交互。

使用场景:适用于在应用程序中进行真实的 HTTP 请求,用于测试整个请求-响应流程,包括与外部服务的交互。

示例用法:

java

Copy code

ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);

assertEquals(HttpStatus.OK, response.getStatusCode());

Feign:

作用:Feign 是一个声明式的 HTTP 客户端,用于简化服务之间的 HTTP 调用。它允许您使用注解来定义 HTTP 请求,将 HTTP 请求映射到接口的方法上。

使用场景:适用于在微服务架构中进行服务之间的通信,通过声明式的方式定义和使用服务接口,类似于调用本地方法。

示例用法:

java

Copy code

@FeignClient(name = "other-service")

public interface OtherServiceClient {

    @GetMapping("/api/someEndpoint")

    String getSomeData();

}

综上所述,在 Web 单元测试中:

使用 MockMvc 可以方便地测试控制器的逻辑,不需要启动服务器。

使用 RestTemplate 可以进行真实的 HTTP 请求,测试整个请求-响应流程和与外部服务的交互。

使用 Feign 适用于微服务架构,用于声明式地定义和使用服务接口,实现服务之间的通信。

其中Feign 与resttemplate 区别

Feign和RestTemplate都是用于在Java应用程序中进行HTTP请求的客户端库,但它们在设计和用法上有一些区别。以下是Feign和RestTemplate之间的主要区别:

1. **声明式 vs. 编程式**:

   - **Feign**:Feign是一个声明式的HTTP客户端,它允许您使用注解来定义HTTP请求,将HTTP请求映射到接口的方法上。您只需定义接口,Feign会根据注解自动生成HTTP请求并进行调用。

   - **RestTemplate**:RestTemplate是一个编程式的HTTP客户端,您需要手动编写HTTP请求的代码,包括构建请求、发送请求和处理响应等。

2. **简化的接口定义**:

   - **Feign**:通过在接口上添加注解,可以轻松地定义HTTP请求的参数、路径、HTTP方法等,使代码更加简洁易读。

   - **RestTemplate**:需要在代码中显式地构建HTTP请求和处理响应,可能会导致代码更加繁琐。

3. **服务发现和负载均衡**:

   - **Feign**:Feign与Eureka等服务发现工具集成得很好,可以自动处理服务发现和负载均衡。

   - **RestTemplate**:需要自己实现服务发现和负载均衡逻辑,或者使用第三方库来实现。

4. **可扩展性**:

   - **Feign**:由于其声明式的特性,Feign在一些特定场景下可能不如RestTemplate灵活,例如自定义请求拦截、添加通用的HTTP头等。

   - **RestTemplate**:由于是编程式的,您可以更灵活地控制请求和响应的处理,满足一些特定需求。

5. **异步支持**:

   - **Feign**:Feign在某些版本中可能有一些有限的异步支持,但不如RestTemplate在这方面成熟。

   - **RestTemplate**:RestTemplate可以更容易地实现异步HTTP请求。

总的来说,Feign适用于构建微服务架构中服务之间的通信,通过声明式的方式来定义和使用服务接口,从而使代码更加简洁和易读。RestTemplate则更适合在应用程序中进行通用的HTTP请求,需要更多的编程控制和灵活性。您可以根据具体需求选择适合的库。同时,随着Spring Cloud的发展,Feign在微服务场景中越来越受欢迎。

所以,集成测试的时候,推荐采用resttemplate的方式

TDD测试驱动开发

测试驱动开发 (TDD, Test-Driven Development) 是一种软件开发方法论,其核心思想是在编写实际功能代码之前先编写测试用例。这种方法有助于确保代码的质量,并且能够持续验证代码的功能是否符合预期。

TDD 的基本流程通常包括以下几个步骤:

编写测试:首先编写一个测试用例,这个测试通常是失败的,因为它测试的是尚未实现的功能。

运行测试:运行测试以确认它确实失败了。

编写功能代码:编写最小量的代码以使测试通过。

重构代码:一旦测试通过,就可以对代码进行重构以改进其结构,但前提是不能改变其外部行为。

重复上述步骤:继续添加更多的测试用例并重复上述过程。

TDD 的好处包括:

提高代码质量:因为每次修改都会伴随着测试的执行,所以可以确保代码的正确性。

更好的设计:TDD 鼓励简洁的接口和模块化的设计。

文档:测试用例可以作为代码的行为文档。

可维护性:易于维护和扩展的代码。

信心:开发人员可以更加自信地进行重构,因为他们知道测试会捕获任何错误。

使用 JUnit 进行 TDD:

JUnit 是一个广泛使用的 Java 单元测试框架,非常适合进行 TDD 开发。下面是一个简单的例子来展示如何使用 JUnit 4 进行 TDD:

创建测试类

public class CalculatorTest {

    @Test

    public void shouldAddTwoNumbers() {

        Calculator calculator = new Calculator();

        int result = calculator.add(1, 2);

        Assertions.assertEquals(3, result);

    }

}

运行上面的测试用例,如果 Calculator 类没有实现 add 方法,那么测试会失败。然后逐步实现功能,直到测试通过。

public class Calculator {

    public int add(int a, int b) {

        return a + b; // 最初的实现可能很简单

    }

}

在实际项目中应用 TDD 时,可能会遇到一些挑战,例如如何编写有效的测试用例、如何处理复杂的依赖关系等。但是随着实践的深入,开发者会逐渐掌握技巧,并从中受益。

最小可行性产品(MVP)

https://zhuanlan.zhihu.com/p/685660604

在当今快节奏的商业环境中,创新成为企业生存和发展的关键驱动力。然而,创新过程往往伴随着高风险、高成本以及市场的不确定性,这使得许多企业在尝试创新时犹豫不决。为了有效应对这些挑战,一种名为最小可行性产品(Minimum Viable Product,MVP)的策略应运而生,并在近年来受到越来越多企业的青睐。

MVP策略的核心思想在于,通过构建一个最小化但功能完整的产品原型,快速验证市场需求和产品方向,从而降低创新风险、减少资源浪费,并加速产品上市时间。在这一策略的指导下,企业能够更加聚焦于用户需求,实现精益创新和敏捷开发。

MVP的定义

MVP是由Frank Robinson提出,并经由Steve Blank和Eric Ries推广的一种精益创业方法论。它指的是用最少的资源和时间开发出来的、具有最基本功能但足以吸引早期用户并获取反馈的产品版本。

MVP的核心理念

快速验证与持续迭代。通过快速推出MVP,企业能够尽早地从真实用户那里获得反馈,然后根据这些反馈不断改进产品,确保最终推向市场的是经过多次优化且更符合用户需求的产品。

MVP在创新中的价值体现

降低创新风险与成本

通过MVP,企业能够在投入大规模资源之前,先以较低成本测试产品概念是否可行,及时调整方向,避免资源浪费。

提升用户参与度和产品粘性

早期让用户参与到产品开发过程中,不仅可以提高他们对产品的忠诚度,还能帮助企业更好地理解用户需求,从而设计出更受欢迎的产品。

加快产品上市速度

MVP强调快速迭代,这使得企业能够在较短时间内完成产品从概念到市场的转化,抓住市场机会

构建MVP的步骤与关键思考

构建MVP是一个系统而严谨的过程,需要遵循一定的步骤和关键思考。以下是构建MVP的详细步骤:

确定目标用户与核心需求

首先,企业需要明确产品的目标用户群体是谁,并深入了解他们的核心需求和痛点。这可以通过市场调研、用户访谈等方式来实现。只有明确了目标用户和核心需求,企业才能确保MVP能够满足用户的期望并产生价值。

设计最小化功能集合

在明确了目标用户和核心需求后,企业需要设计一个最小化但功能完整的功能集合。这个功能集合应该包括满足用户基本需求所必需的功能,同时去除任何非核心或不必要的功能。这样可以确保MVP的简洁性和易用性,同时降低构建成本和时间。

快速原型制作与测试

接下来,企业需要利用现有技术和资源快速构建MVP原型,并进行实际测试。这个过程可能涉及原型设计、代码开发、用户测试等环节。通过测试,企业可以收集到用户的反馈和数据,了解MVP在实际使用中的表现和问题。

收集用户反馈与数据分析

在测试阶段,企业需要积极收集用户的反馈和数据,并进行深入的分析。这些反馈和数据可以帮助企业了解用户对产品的真实感受和需求,发现产品中存在的问题和改进空间。同时,这些数据还可以为企业后续的迭代优化提供有力的支持。

迭代优化与产品调整

基于用户反馈和数据分析的结果,企业需要对MVP进行迭代优化和产品调整。这可能涉及功能增强、性能提升、用户体验优化等方面。通过持续的迭代优化,企业可以不断完善产品并满足用户不断变化的需求。

在构建MVP的过程中,企业还需要注意以下关键思考:

始终保持以用户为中心的设计理念,确保MVP能够满足用户的真实需求。

平衡好速度与质量的关系,既要追求快速验证市场反馈,又要确保MVP具有一定的稳定性和可用性。

充分利用现有技术和资源,降低构建MVP的成本和时间。

建立有效的反馈循环机制,确保能够及时收集和分析用户反馈,并将其转化为有价值的产品改进方案。

MVP策略的实施难点与解决方案

 尽管MVP策略在理论上看似简单直接,但在实际实施过程中,企业可能会遇到一些难点和挑战。以下是对这些难点及其解决方案的探讨:

难点一:确定核心功能的界限 在构建MVP时,企业需要准确判断哪些功能是核心的,哪些功能是可以暂时省略的。这是一个具有挑战性的任务,因为对核心功能的定义可能会因用户需求、市场趋势和企业战略的不同而有所变化。

解决方案:

深入进行市场调研和用户访谈,以了解用户的真实需求和期望。 与内部团队进行充分的沟通和协作,确保对核心功能有共同的理解和认同。 保持灵活性,根据市场反馈和用户需求的变化及时调整核心功能的定义。

 难点二:快速原型制作的技术限制 在某些情况下,企业可能面临技术限制,无法快速构建出满足要求的MVP原型。这可能是由于技术团队的能力不足、现有技术的局限性或时间紧迫等原因造成的。

解决方案:

提前评估技术团队的能力和资源,确保他们具备构建MVP所需的技术和工具。 在必要时寻求外部技术支持或合作伙伴的帮助。 优先考虑使用成熟的技术和框架,以加快原型制作的速度。

难点三:用户反馈的收集与分析 收集和分析用户反馈是MVP策略的关键环节之一,但这也是一个具有挑战性的任务。用户反馈可能是模糊的、分散的或相互矛盾的,需要企业进行有效的整理和分析。

解决方案:

设计明确的用户测试计划和反馈收集机制,确保能够系统地收集用户反馈。 使用数据分析工具和技术,对用户反馈进行定量和定性的分析。 建立跨部门的反馈处理团队,确保用户反馈能够及时得到响应和处理。

MVP策略与其他创新方法的结合应用

 MVP策略并不是孤立的创新方法,它可以与其他创新方法结合应用,以实现更好的效果。以下是一些建议的结合应用方式:

与敏捷开发方法结合 敏捷开发方法强调快速响应变化、持续交付价值和紧密协作。将MVP策略与敏捷开发方法结合,可以使企业在创新过程中更加灵活、高效地应对市场变化和用户需求。 与设计思维结合 设计思维是一种以人为本的创新方法,强调从用户的角度出发,发现问题并寻找解决方案。将MVP策略与设计思维结合,可以帮助企业更深入地了解用户需求,并构建出更符合用户期望的产品原型。 与持续集成和持续部署(CI/CD)结合 持续集成和持续部署是一种自动化的软件开发和部署方法,可以加快软件产品的交付速度。将MVP策略与CI/CD结合,可以使企业在构建和测试MVP原型时更加高效、可靠地进行代码集成和部署。

MVP策略在创新过程中具有重要的应用价值。通过详细解析MVP的定义、核心理念、价值体现以及构建步骤与关键思考,并结合实施难点与解决方案以及其他创新方法的结合应用,企业可以更加深入地理解并应用MVP策略来推动创新项目的发展。

代码生成器

Json分为;类的结构图

整个项目的属性

单表的属性(如果表有关联的多表,那么就在关联的多表属性)

字段的属性

Jpa的客户端作为一个starter

生成的代码,作为一个starter

开发方式1.0

传统的开发方式

完全自己写各层

开发方式2.0

ER关系设计

手动写SQL语句

IDEA插件自动生成各层代码

如果有变更字段,就在Entity中,增加字段

开发方式3.0

类设计

自动映射DB代码

调用模板自动生成各层代码

触发CICD的动作,直接进行部署

优点:敏捷迭代

开发与管理进行结合

一般的业务具有需求不稳定性,尤其在项目初期,需要不停的试错。这个时期的敏捷性开发尤为重要。尤其是不稳定业务特征下的敏捷开发。

代码生成器是一个工具,在敏捷性开发中能发挥重要作用,但是在使用过程中,经常会出现,第一次使用的时候,采用代码生成,后期的变更,只能在生成过的代码中进行调整。导致后期业务变更的时候,因为用户自己写的代码与原始代码混合在一起,无法再次生成代码。代码生成器的作用就没有充分发挥。使得敏捷开发在实际执行的时候比较困难。

同时当前业界流行的DDD开发方式,领域驱动的开发。TDD的开发方式,测试驱动开发的方式。可以使用当前作业方式有机的融合,达到团队的开发及变更效率比较高的状态。

原始自带的模板

原来JeecgBoot自带的模板Controller,Service,Dao,Entity,vue

一般的应用程序都采用分层架构,分层的代码具有高度相似性。参考Mybatis的代码生成器的定义,代码生成器的模板。

controller,控制层,负责具体模块的业务流程控制,需要调用 service 逻辑设计层的接口来控制业务流程。因为 service 中的方法是我们使用到的,controller 通过接收前端 H5 或者 App 传过来的参数进行业务操作,再将处理结果返回到前端。

entity, 实体层,用于存放实体类,与数据库中的属性值保持一致,实现set和get的方法 ;

service,业务 service 层,给 controller 层的类提供接口进行调用。一般就是自己写的方法封装起来,就是声明一下,具体实现在 serviceImpl 中;

Dao,对数据库进行数据持久化操作,它的方法语句是直接针对数据库操作的,主要实现一些增删改查操作,在 jpa 中体现为repository;

新增的模板

树的处理,返回一个完整的树

WebSocket

根据业务需要进行补充新增的模板

JPA的支持

这个部分-暂时推迟一下

新增模板,Controller,Service,Dao,Entity,vue

模板Entity可以为三种类型

先Domain,单表操作。外键定义的规范采用对方类名+_id

建表之后,推断物理外键

通过物理外键,生成ManyToOne,OneToMany的注解

mybatis一样,单表存在

这种在项目中应用的比较多

ManyToOne的单方向映射

这种在项目中应用的比较多,扩展的时候,比较方便

ManyToOne与OneToMany同时存在

扩展不方便,适合与查询工具的使用

构建新的JPA的模板,然后根据ER关系图,算法生成双向的注解。

针对查询类的需求。是否满足的速度会很快,因为有类的导航处理。

ManyToOne与OneToMany同时存在,且外键也存在

映射关系上使用@Transist

目标:实现类之间的导航,使得业务逻辑能敏捷实现。如效果不高,可局部进行重构。

JPA与Mybatis的混合支持模板-暂时不处理

在Entity中,有Mybatis及JPA的注解同时存在。

有部分方法,采用JPA处理,有部分采用Mybatis处理。

事务采用JPA事务。

前端界面的结构

页面查询

页面分为4个主要组成部分,查询区,按钮区,列表区,新增区。比较稳定的规律,看进行生成。

单表树

大屏

一个大屏,分为多个区,每个区单独进行生成模型

框架配合

JPA的依赖

JPA的表名自定义接口

@ApiVersion的注解

开发规范

表名:英文小写,不用复数,不用数据库的保留字select、where、desc、range、match、delayed等

整数,选择Integer

小数,选择BigDecimal

日期,选择Date,格式为XXX

日期,选择DateTime,格式为XXXX

Text,大文本

Blob,二进制

第一版本

业务人员:

构建原型

业务人员根据Word文档或PPT参考内容,使用Axure等工具构建业务原型。原型的种类有3种。

管理类界面的风格

界面分为4个组成部分

查询区、按钮区、列表区、弹框编辑区

根据这样的界面,每个区域进行建模。列表区是最全的。所以各区建VO是重叠的。取并集。主题是一个。所以是单VO操作

可以构建VO的模型为

Id,username,age

在这个模型下,可实现CRUD的操作

大屏类界面的风格

针对每个区域进行建模。主题是多个,所以是多VO操作

Echart类型的VO处理方式

如果是列表类的数据,那么就采用业务人员低代码平台来进行处理。

如果是折线图的方式

介于2者之间的风格

针对每个区域进行建模,主题多个,多个vo操作

低代码工具生成界面

界面的数据构建。

界面分为多个区域,如果是查询

根据业务的原型构建VO类。根据VO类,

一般低代码生成工具,有web操作界面,在界面中,业务人员进行页面操作。

页面操作的规范

从页面操作的时候,有5个字段是规范字段,不需要调整。

为了避免字段类型转换,全部采用小写字母来命名?

自动生成表,表的命名,进行代码生成器引擎拦截。增加一个默认的前缀,如

业务名称_VO_表名-- 李炎戌处理。

关联关系不需要设置。每个表对页面人员来讲,是平行的。

该版本,可以无限次变更。

构建Mock数据

本版本的目的,重点是保持Controller层的稳定。

该版本生成的代码,不允许用户修改。必要的时候,可以构建为springboot的Starter的依赖。

业务人员生成的代码位置为,作为规范,李炎戌确定

生成代码之后,将代码提交到git服务端,然后进行docker化部署,配置权限。每次用户提交之后,就能通过网页访问最新的版本。

开发人员

Echart的表,格子转换的问题

Echart的折线图
百度的原始版本

新的版本

返回格式与折线图一样

数据集 - 概念篇 - 使用手册 - Apache ECharts

当前部门内封装的格式如下--与百度的版本保持一致

返回JSON的服务端代码

public class XAndManyYReportIntegerDto {

@ApiModelProperty(value = "x轴")

private List<String> xAxis;

@ApiModelProperty(value = "y轴map:key为y轴名称,value为y轴list数据集合")

private Map<String, List<Integer>> yAxisMap;

}

Map映射到json的时候,是一个对象。Key作为该对象的属性。Value是该对象属性所对应的值。

返回的JSON格式

Datasource的值,建模EntityVo表

//该转换工具,在用户请求之后,从数据库读入之后,进行返回固定的格式

    //业务人员建表规范:x轴采用x开头,y轴采用y开头

    private XYDto convert(List<EntityVo> entityVoList){

        XYDto xyDto=new XYDto();

        for(EntityVo each:entityVoList){

            //如果属性名称开头为x,那么设置xyDto的值xAxis

            //如果属性名称开头为y,那么设置xyDto的值yAxisMap

            //补充一下算法

        }

        return xyDto;

    }

    @Data

   public class EntityVo{

        private DateTime xDate;

        private String yCategory;

        private Double aDouble;

    }

    private class XYDto {

        private List<String> xAxis;

        private Map<String, List<Integer>> yAxisMap;

    }

如果是饼图,那么返回的值就不用进行处理

页面的建模规范中,采用这种格式来进行建模处理。

Echart的柱状图

Echart的饼状图

Echart的表格图
非表格化的返回结果

不是list的结构,是返回的Json的结构。上面有2个统计值,下面是列表的方式

第二版本

开发人员

从表转为类

根据第一版的产生的数据库表,采用工具或服务转为类,类中没有映射关系。类名为

表名。这个转换工具修改一下,将表名与类名进行映射一下。

在类中进行增加字段,删除字段,修改字段。然后运行程序,根据JPA的机制,重新生成数据表。

在生成数据表的时候,继承SpringPhysicalNamingStrategy,自动修改表名,表名前缀为业务名称_ER_表名。

在生成的类代码中,增加一个注释,

该版本,可以无限次自动生成。

生成的代码位置,李炎戌确定

版本对比

建立一个定时任务:将业务领域专家写出来的表,每天记录一下表名及字段名称。然后与上一个版本进行比较。结果存入到表中,利用JeecgBoot自动生成一个查询界面。---已经安排颜建斌处理。

第一版变更之后,与第二个版本进行比较,比较表名及字段及时提醒。

对比的目的,是根据领域专家调整的内容,来考虑ER关系的调整。为了保持相对稳定性,要求领域专家在调整的时候,进行记录到禅道或某一个位置。推送到相关人员都知道。

自动构建外键

为避免JeecgBoot系统库的影响,将ER表复制到新的数据库中。

根据表中字段,自动推断外键关系,建立之后,在Navicat中观察ER关系。如果不满足,重新调整类来生成。

在Navicat中观测的ER关系图类似如下

构建Mock数据

在JPA类生成的时候,同时使用注解,构建Mock数据。

自动删除外键

ER关系满足要求之后,删除外键。删除外键的服务-已经安排颜建斌处理

该版本生成的代码,不允许代码人员修改。必要的时候,可以构建为一个springboot的Starter的依赖。

本版本的目的,重点是保持Service,Dao层,方便调用。

第三版本

开发人员

路由调整

调整Contoller接口增加ApiVersion标记

复制需要Contoller的接口

调用第二版本的Service及Dao

大屏的情况下

只要转成Dto就可以了。根据上面的转换关系,就自动转换为返回的接口类型。

代码编写及xml格式编写

单元测试

写单元测试,满足每次变更的时候单独运行

总结一下:

第一个版本:主要根据页面进行建模

定义VO

构建模拟数据

定义Controller层的接口,部署一个可以访问的接口服务

第二个版本:

根据第一个版本的数据表,使用IDEA的工具,生成类。然后在JPA的客户端进行调整,增加ManyToOne,OneToMany的标签。生成数据表

同时生成模拟数据。这期间不需要业务逻辑编码。

第三个版本:

在没有使用本版本的时候, 用户仍然访问的是第一个版本构建的模拟数据返回的接口。

增加接口层重写。复制第一个版本的Controller,然后注入第二个版本的service,然后进行重写。

同时写一下测试用例。这个版本不允许被覆盖。

第一个版本,第二个版本,可以被任意覆盖。但是第三个版本,不能被覆盖。一直用于维护。

例子

标准的CRUD的界面

有一个前端界面

上面是查询

中间是按钮

下面是列表

项目编号 prono,

项目名称 projname,

子项编号 subprojno,

子项名称 subprojname,

子项金额 subproamount

  1. 页面建表b_project,代码生成
  2. 将该表转为jpa的类名为project

注意:生成的类的模板上不要有@Table注解,是因为jpa会自动添加前缀

3、将project类中保留

项目编号 prono,

项目名称 projname,

新建subproject类,同时将

子项编号 subprojno,

子项名称 subprojname,

子项金额 subproamount

复制到本类中

然后增加外键,Project_id

  1. 生成表,jpa_project,jpa_subproject
  2. 根据字段属性,自动匹配外键约束关系,并增加外键关系
  3. 生成全套代码,同时生成mock数据
  4. 重写Controller的新版本,注入第二次的service,来返回前端

大屏展示页面

大屏的每个区单独进行建表。

针对Echart返回的内容,可以总结模型,定制freemaker来完成模板更新,返回给前端合适的接口。

附录-参考原始文档

@apiversion的实现方式

定义版本号

首先,需要确定API的版本号方案,比如使用整数、字符串等作为版本号。可以根据业务需求和实际情况确定版本号的格式和规则。

为了版本化对比,一般情况下,采用整数的方式

自定义注解

import org.springframework.web.bind.annotation.Mapping;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Mapping

public @interface ApiVersion {

    int value() default 1;

}

版本选择逻辑

在用户自定义的HandlerMapping中编写逻辑,根据请求中的版本号信息,选择对应的处理器方法进行处理。

定义ApiVesrsionCondition继承RequestCondition

import org.springframework.web.servlet.mvc.condition.RequestCondition;

import javax.servlet.http.HttpServletRequest;

public class ApiVesrsionCondition implements RequestCondition<ApiVesrsionCondition> {

    private int apiVersion;

    public ApiVesrsionCondition(int apiVersion){

        this.apiVersion = apiVersion;

    }

    public ApiVesrsionCondition combine(ApiVesrsionCondition other) {

        // 采用最后定义优先原则,则方法上的定义覆盖类上面的定义

        return new ApiVesrsionCondition(other.getApiVersion());

    }

    public ApiVesrsionCondition getMatchingCondition(HttpServletRequest request) {

        //String ver = request.getHeader("Api-Version");

        if( request.getHeader("Api-Version") ==null){

            return this;

        }else {

            String ver = request.getHeader("Api-Version");

            //因为请求头里面传来的是小数,所以需要乘以10

            //int version = (int) (Double.valueOf(ver) * 10);

            int version=Integer.valueOf(ver);

            if(version >= this.apiVersion) // 如果请求的版本号大于等于配置版本号, 则满足

                return this;

        }

        return null;

    }

    public int compareTo(ApiVesrsionCondition other, HttpServletRequest request) {

        // 优先匹配最新的版本号

        return other.getApiVersion()-this.apiVersion ;

    }

    public int getApiVersion() {

        return apiVersion;

    }

}

重写mapping

import org.springframework.core.annotation.AnnotationUtils;

import org.springframework.web.servlet.mvc.condition.RequestCondition;

import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;

public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    protected RequestCondition<ApiVesrsionCondition> getCustomTypeCondition(Class<?> handlerType) {

        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);

        return createCondition(apiVersion);

    }

    @Override

    protected RequestCondition<ApiVesrsionCondition> getCustomMethodCondition(Method method) {

        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);

        return createCondition(apiVersion);

    }

    private RequestCondition<ApiVesrsionCondition> createCondition(ApiVersion apiVersion) {

        return apiVersion == null ? null : new ApiVesrsionCondition(apiVersion.value());

    }

}

注册到Spring中生效

@Component

public class WebRequestMappingConfig implements WebMvcRegistrations {

    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {

        RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping();

        handlerMapping.setOrder(0);

        return handlerMapping;

    }

}

客户端访问方式

  1. 客户端url不变

默认情况下,访问服务端url对应的最高版本

  1. 客户端自定url版本号

客户端在请求的时候,在Head中,加一个Api-Version字段,写明版本号。如果没有这个版本号,那么就默认是最高版本号。

服务端设置方式

定义处理器方法:在Controller中定义多个处理器方法,每个方法对应一个API版本。可以使用不同的注解或URL映射来标识不同版本的处理器方法。

如果不加自定义注解,正常情况下,url是不能重复的,否则springboot是不会启动成功的。

同一个url,但是版本号不一样。这个是springmvc定义的

注意在一个类中,同一个url但是方法名不能一样。在一个类中,方法名必须不一样,这个是jvm规范定义的。

@ApiVersion(1)

@RequestMapping("/test")

    public void test1() {

    System.out.println("method---test1");

}

@ApiVersion(2)

@RequestMapping("/test")

    public void test2() {

    System.out.println("method---test2");

}

在上述步骤中,自定义HandlerMapping起到了关键的作用。它负责根据请求的版本号选择合适的处理器方法,实现了对不同版本API的控制和管理。

自定义表名实现方式

某些场景下,可以通过自定义命名策略来简化操作,或实现自身特定的业务,例如:需要在表之前,增加统一的前缀如jpa_。

1、继承SpringPhysicalNamingStrategy 接口,然后覆盖需要实现的方法。

public class CustomPhysicalNamingStrategy extends SpringPhysicalNamingStrategy {

    @Override

    public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) {

        String customName = transformName(name.getText());

        return StringUtils.hasText(customName) ? Identifier.toIdentifier(customName) : name;

    }

    private String transformName(String name) {

        // 在这里实现您的自定义逻辑来转换命名规则

        // 例如,将驼峰命名转换为下划线命名

        return "jpa_"+name;// 自定义逻辑

    }

}

2、在 application.yml 中修改如下配置

  jpa:

    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

    show-sql: true

    hibernate:

      ddl-auto: create

      naming:

        # 3. 自定义的命名策略

        physical-strategy: cn.mariojd.jpa.naming.config.CustomNamingStrategyConfig

问题

1、当前采用Jeecgboot的拦截内容,不需要JPA的特征。直接进行生成

下个星期的安排

李炎戌:

低代码平台的一个例子完成,2个表Proj,Subpro

李桐:

原型形成第一个版本

前端:

Bimface等的应用

单表的应用

Jeecg的拦截处理,这样就可以实现外键来进行处理。

树,采用Jeecg自带的功能,形成一个技术组件来进行处理。

快速开发适用的场景,用于快速迭代。

如果产品已经逐步成型,有稳定的类关系,那么就不需要快速几个版本迭代了。

代码生成器的实战

代码下载及运行

Git下载Vue3代码

访问地址

https://gitee.com/jeecg/jeecgboot-vue3?_from=gitee_search

点击,克隆,然后点击复制

右键

Git bash here

人工智能的CodeGeex及通义千问

在命令行上

输入 git clone  https://gitee.com/jeecg/jeecgboot-vue3.git   (注意:clone后面的内容,可以采用粘贴的方式)

等待1分钟左右,前端代码下载完成。

下载Java服务端代码

地址为https://gitee.com/jeecg/jeecg-boot

操作步骤与上面一样

服务端进入编译环境

在本地数据库创建jeecg-boot数据库

执行划线的脚步

设置Redis的密码XXX

启动RedisServer

我的本机目录在D:\Redis-x64-3.2.100

windows系统下启动redis命令

  1. 打开命令 window+r 输入cmd
  2. 进入redis安装目录 cd 到redis的安装目录
  3. 输入redis-server.exe redis.windows.conf 启动redis命令,看是否成功
  4. windows系统下启动redis命令
  5. 可能会启动失败,报28 Nov 09:30:50.919 # Creating Server TCP listening socket 127.0.0.1:6379: bind: No error
  6. 报错后,输入redis-cli.exe

6输入shutdown结束

7输入exit退出

8,继续输入redis-server.exe redis.windows.conf启动redis命令,启动成功。

用Idea打开项目—有POM文件所在的目录

打开这个文件,配置mysql的密码及redis的密码

右键点击运行

出现这个界面,表明服务端运行成功。

客户端进入编译环境

有右下侧会提醒安装一个内容,大概2分钟

浏览器访问

http://localhost:3100/

出现这个界面,表明前后端启动成功

程序开发

新增数据表

然后同步到数据库中,根据配置项生成数据表

生成代码

将java服务端代码复制到demo目录下

将Vue的代码复制到前端的代码文件下

重新启动IDEA及WebStorm。

在系统中添加菜单及权限

单表的开发完成。

并发导致的问题

计算的结果,应该调用次数,并发次数无关,最后的结果一样的

sum=100+sum

sum=100+sum

时刻

响应第一个客户请求的工作线程1

响应第一个客户请求的工作线程2

T1

读取请求参数increase=100

T2

睡眠3-1秒

读取请求参数increase=200

T3

睡眠3-2秒

睡眠3-1秒

T4

睡眠3-3秒

睡眠3-2秒

T5

获取实例变量sum的100,然后sum赋值=100+100

睡眠3-3秒

T6

向客户端返回sum值

获取实例变量sum=200+200

T7

向客户端返回sum=400

这个结果计算有问题,应该是300,不应该是400,

新手培训指南

第一天

Mybatisplus 实现一个user的web服务。

HowTomcatWorks第一章,构建一个Socket的通信

第二天

Db操作,原来的题库,设计ER关系图,提前的作业

MybatisPlus实现1-2个查询,构建一个1到100 相加的服务。

第三天

MybatisPlus实现多个表的查询构建一个服务

JeecgBoot代码生成器

HowTomcatWorks 第二、三章 ,面向对象,socket,架构设计的原理

HowTomatWorks第四、五章,程序的扩展性

第四天

设计模式的几个模式

HowTomcatWorks中,设计模式的应用

一款类似于SpringMVC方式的websocket框架 - 房东的Tom - 博客园

一款类似于SpringMVC方式的websocket框架

http://www.taodudu.cc/news/show-4017515.html?action=onClick

WebSocket通信原理和在Tomcat中实现源码详解(万字爆肝)

Tomcat架构详解-CSDN博客

tomcat之源码分析

tomcat之NIO模型(NioEndpoint)及Connector相关配置_tomcatnio模型-CSDN博客

tomcat之NIO模型(NioEndpoint)及Connector相关配置

https://blog.51cto.com/u_15707676/5446160

HTTP请求中的Keep-Alive模式,是怎么区分多个请求的?

构建测试的能力是理解原理的能力。如果测试能力能构建好,说明,程序会构建的很好,质量高,有效率。

设计模式的应用,把书读薄类似

从架构的ppt到实现的中间关键过程

数学与计算机的关系

数学之美

2024年9月3日星期二

建模的时候,采用JPA的处理,CodeFirst;一直维护这个领域模型

在这个领域模型中2种方式

第一种方式:类独立的维护外键字段

第二种方式:类将独立的外键字段转为另外一个类,然后加@manytoone及@joincolum注解

每次变化的时候,执行2个动作

第一个动作:

生成DB,这个是jpa本身的动作

第二个动作:

生成全套的前后端代码,覆盖之前的代码

每次自己写的代码,要放入到一个新的地方,不要与单表放在一起。

Another way

在Web开发领域,基于页面的建模方式。根据页面来设计Java的接口层VO层。然后到DB中。然后生成第一个版本

工具的VO的类,始终维护这个版本变化。如果变化,就重新生成,然后服务端进行CICD。重新覆盖。

自己写的代码,采用新的版本,url保存一致,在新的类中。通过service层的组合来实现这个接口代码。

最小可行性产品(MVP)在创新中的应用与实践


网站公告

今日签到

点亮在社区的每一天
去签到