新冠病毒🦠还在阻挡全世界重启,但我们学习脚步不不能停滞,接下来给大家展示一个现在开发中已经不太常用的一个小知识点,希望对大家有所启发。
在平时 大家可能用 Spring Boot 2 最多就是开发 RESTful API,可能很少有人在 Spring Boot 2 中用过JSP视图,那我就来一起体验下创建一个用 JSP 视图的 Spring Boot 2 应用有多么方便。
咱们可以从 Spring Initializer 获取项目框架。
pom.xml
|
SpringBootServletInitializer
按传统的 WAR包 部署方式来运行 SpringBootJspApplication
。
SpringBootJspApplication.java
package com.eprogrammerz.examples.spring.springbootjsp; |
spring.mvc.view.prefix: /WEB-INF/jsp/ |
写一个简单映射方法的Controller
package com.eprogrammerz.examples.spring.springbootjsp.controllers; |
将下面内容保存成 JSP 文件,放在src/main/webapp/WEB-INF/jsp/
目录
|
在项目根路径下使用命令行运行程序。
mvn clean spring-boot:run |
访问 localhost:8080 测试你的程序效果。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope></dependency>
本文主要介绍自己第一次使用Memcached时的奇葩遭遇,由于自己是使用Memcached的小白,第一次遇到这种问题,感觉很是难想到最根本的原因,在这里记录一下,希望能帮到用Python和Java同时读写Memcached时遇到同样问题的同学。对于这个问题其实弄懂了很简单,网上也有很多对于这个问题的解释,总感觉都写的不太透彻,这里会从问题根源以及最佳实践给大家做个简单介绍,从而让大家能选择更适合自己的解决方案。
今个是新中国成立70年的国庆日,本文作者满怀欣喜地全程观看了国庆阅兵仪式。在为新中国取得了这么多举世瞩目的成就的同时,程序猿直男总是要犯病,该总结的问题还是要及时总结的。下面将给大家讲讲我前不久遇到的一个十分诡异的事情,希望能遇到同样问题的童鞋有所帮助。
公司领导想做一个日志解析展示的程序,至于技术选型就不细说了,日志分析使用Python,数据临时存储使用Memcached,数据展示使用Java作为后端提供API给前端展示。
为了大家也能更好的理解Python写Memcached但Java读不到的这个问题的情景,这里先给大家复现一下。下面将从Memcached安装、Python客户端&代码、Java客户端&代码、以及最后复现出的问题现象进行描述。
在Linux环境下安装Memcached的步骤如下
1.安装libevent库 |
出现此问题时使用的Python客户端为python-memcached,下面为使用pip安装的命令
pip install python-memcached==1.53 |
# write-into-memcache-opration.py |
出现此问题时用的Java客户端为Memcached-Java-Client,以下为使用maven安装的依赖版本
<dependency> |
// MemcachedJavaClientTest.java |
python write-into-memcache-opration.py |
执行上面命令后,结果如下图:
从上图可看出数据已经写入。
编译 |
执行上面命令后,结果如下图:
从上图看出Java并没有读取到Memcached中key为site的数据,而是返回null。
从上面python读取Memcached到数据和Memcached命令行中读取到的数据均可证明:key为site的数据确实已经写入到Memcached里了,但为啥Java客户端读取不到呢?难道见鬼了?这到底是咋回事?
使用Python的python-memcached客户端向Memcached写入一个value为字符串的值,但使用Java的Memcached-Java-Client读取Python刚写入的字符串值却反回为null。
对于这个问题可能的原因有:
显然我现在已经黔驴技穷了,但问题还得解决,继续硬着头皮继续百度&Google吧。经过一番折腾所有搜索的线索均指向了flags这个东西,这是个啥呢,那就先来深入了解下这个flags再说。
flags这个参数其实是让客户端给自己的字符串数据打一个label标记罢了,就这么简单。由于memcached存放的数据类型只有string一种,那么memcached不知道你存的数据到底是序列化的字符串还是普通字符串,那么你设置一个flags,等你使用get拿到string的时候,也把这个flags给你,让你自己判断进行处理。目的就是为了方便各个语言版本的客户端例如 Java Python PHP等等,可以通过存储序列化后的字符串,达到存放数据的目的。 这样的话客户端可以根据自己给字符串设置的flags,判断例如是否要反序列化等等操作。 对于调用memcached的客户端库是透明的,用户无感的,感觉就是使用set/add把一个变量存进去, 后面使用get获取这个变量就行了。其实客户端库底层就是通过这个flags帮你实现的,不过各个语言的实现可能不同,但是原理是一样。
例如php的memcache扩展,底层就帮我们把这个实现细节屏蔽了。通过add/set存对象(object),数组(array)都会自动序列化, 取数据会反序列化。 但是对于string int double bool只是就直接存了。 所以其实在memcache扩展中,我们一般情况下直接把flags设置false即可。实在是用到flags的话,只能填一些参数 如 ture false null 以及压缩常量MEMCACHE_COMPRESSED表示压缩数据,其他值可能会报warning错误或者致命错误。
从以上对flags含义的描述,大致可猜测到Python想Memcached写入字符串但Java读取不到可能是两种语言使用的客户端实现读写字符串时使用的flags不同,导致一种语言写入另一种语言读取解析不出来的。为了进一步验证确实是flags的原因,下文将从不同语言客户端实际写入和源码两方面验证这一问题。
从上面两个图中可以看出使用Python和Java客户端写入Memcached时使用的flags确实不同,Python使用的flags是0,Java使用的flags是32,进而可以猜测得知不同语言的客户端在反序列化时具体要转换成哪种类型的数据是要通过写入时设置的flags来判断的。接下来为了验证这一结论,下文将从不同的语言的客户端的源码实现中来验证。
接下来将贴一些客户端的实现源码来分析从Memcached中读取的数据是怎样通过不同的flags值来反序列化出不同类型的数据的。
下面将列出Python客户端python-memcached写入Memcached是源代码,来验证写入
# 在set()方法中主要调用了 _val_to_store_info(self, val, min_compress_len)方法将即将存储到Memcached中的数据进行转换并生成对应的flags值 |
从上面python源码可以看出,对于字符串类型的数据在写入Memcached时,客户端python-memcached使用的flags是0,这和Memcached命令行get数据得到的flags是一致的。
接下来,咱们看一下如何通过flags来反序列化数据的,下面代码为Memcached Client for Java客户端读取序列化后对象的过程
// @see com.schooner.MemCached.AscIIClient.java |
从上面反序列化的流程可以看到主要是通过NativeHandler.decode(buf, flag)
进行反序列化的,接下来在看下这个函数的代码
// @see com.schooner.MemCached.NativeHandler.java |
从上面的代码可以看出当flags为32时,Java版Memcached Client for Java客户端会将从Memcached读取的数据反序列化为字符串。这一结论也和Memcached 命令行中得到Java版客户端将字符串写入Memcached时生成的flags是一样的。
为了进一步验证确实是由于python客户端和java客户端在读写字符串时使用的flags不同导致的这个问题,现在对Python客户端python-memcached进行临时修改来再次实验使用Python写入Java是否能读取到数据?修改的代码如下
memcache.py是修改后的Python源码,memcache.py.bak是原始文件。(为了保证上面复现问题用到的Python代码不修改仍能使用,此处将python读取时用的flags一并改成32) |
接下来执行python write-into-memcache-opration.py
,正常写入,接下来执行java MemcachedJavaClientTest
,读取也正常了。这下所有的疑惑都烟消云散,就是由于不同的语言的客户端在实现序列化和反序列化时使用flags代表的数据类型不一样或者使用的flags的值不同导致。
从上面使用Python和Java分别写入Memcached再通过Memcached命令行get数据验证以及查看Python客户端写入和Java客户端读取的源码可以知道,Python客户端python-memcached读写Memcached中的字符串类型数据时flags使用的是0;Java客户端Memcached Client for Java读写Memcached中的字符串类型数据时flags使用的是32,所以造成了Python写入Memcached后Java却读取不到的问题,更确切地说,Java实际已经读取到了Python写入Memcached的数据但根据flags为0解析读到的数据时,因为没有flags=0的情况而导致解析数据报错,从而最终得到null。
在考虑可行性方案时首先我们要了解下市面上已有客户端对flags的实现情况(仅列出了读写字符串类型时的flags,至于其他类型的,哈哈,别犯懒,自己补充吧),然后再根据实际情况决定具体的合适的可行方案。
名称 | 特点 | flags含义 |
---|---|---|
python-memcached | python编写、 对序列化和反序列化有灵活的方法、 不实现”noreply”、 比pylibmc和pymemcache慢 | flags=0表示字符串 |
umemcache | C++语言实现 | (安装报错) |
pylibmc | C语言实现、快速、 实现一致的哈希、 不能访问“noreply”标志、 非常依赖libmemcached | flags=0表示字符串 |
pymemcache | 支持”noreply”访问(提到写入速度)、 模块化和简单的序列化和反序列化的方法 | flags=0表示字符串 |
名称 | 特点 | flag含义 |
---|---|---|
Memcached Client for Java | 版本较早、 应用广泛、 运行较稳定、 使用阻塞IO、 不支持CAS操作 | flags=32表示字符串 |
SpyMemcached | 使用了concurrent和nio 读取速度更快、 支持异步、 支持CAS操作、 稳定性不好 | flags=0表示字符串 |
XMemcached | 使用NIO 效率高、 在数据小时比SpyMemcached更优秀 | flags=0表示字符串 |
从上面两个表格罗列的Python、Java客户端使用flags定义来看,对于字符串类型的数据来说,只有Java客户端Memcached Client for Java在存储字符串类型的数据时flags使用的32,哇哦,咋这么悲催,这么巧的诡异事情咋让本尊遇到了,如果上来就使用的另外两种Java客户端也就没这么大费周折了,当然祸兮福所倚,如果没有这么折腾也就没有对Memcached对这么深刻的理解了也就没有这篇文章的分享了(来碗鸡汤:平时开发中,大家不要苦恼那些遇到的坑,没有这些坑的历练可能我们也很难有坚实的进步)。
说到这里,其实对于本文作者已经有了合适的解决方案,因为作者开发的项目主要在Memcached中存储字符串类型的数据,所以最合适最明智的做法就是更换Java客户端来适应Python客户端,这样对开发的项目也更有普适性。但为了脑洞大开下,并且本文中并没有讨论除了字符串类型之外的数据类型,so下面简单介绍下其他可行的方案,希望能给大家抛砖引玉。
这一小节将给大家简单介绍下,对于不同客户端使用不同flags值表示不同类型数据,这里给大家罗列一些可行的解决方案,希望对有同样问题的童鞋有帮助。
对于修改客户端源码,上面【修改Python写入Memcached时使用的flags验证Java是否可读】小节中已经使用过了,由于修改Python源码比较简单,所以大家也可以尽量修改Python源码,当然也可以修改Java源码进行编译,来让用到的客户端能相互识别所存储的数据类型。
对于修改源码这种方案,有个尴尬的问题时,我们修改源码后需要上传自己的Python/Java包到远程库,这显然是不方便也不利于部署,除非自己搭建了专用的包管理库。
这种方案需要针对项目中使用到数据类型来寻找对flags实现一致的客户端,这种方案应该是最方便也最可靠的可行方案。为了安全起见,这需要对使用到的所有数据类型进行读写测试,来保证方案的全面适用。
这个方案更适用于对Java更熟悉的童鞋,大致看下源码不难看出通过flags解码的流程主要在SerializingTranscoder.java类中decode()方法中,具体的代码这里就不给贴了,可以重写下这个方法来实现同Python客户端一致的flags实现。(如果对重写解码流程有兴趣,可以参考下文章 flag – 诡异的memcache标记)
对于Python写入Memcached但Java读不到这个问题的分析就到这里了,总结一句,这个问题就是由于不同客户端使用不同的flags值进行序列化&反序列化导致的。弦外音,对于平时感觉很诡异的问题,主要的原因还是对使用的一些工具不求甚解的原因,网上的讲解可能也不太透彻,当然也包括本文,希望遇到类似问题的童鞋还是要多静下心来寻找问题的蛛丝马迹,主要多从基础问题基础概念上入手逐步深入,这样才能拨开云雾。
]]>如果你是一名Java开发人员并且对技术充满热情,那么你可以遵循以下十个步骤,这些步骤可以助你成为优秀的Java开发人员。
对于Java开发人员来说,必须具备对面向对象编程的深刻理解。如果没有OOPS的坚实基础,就无法实现像Java这样的面向对象编程语言的美感。如果您不太了解OOPS是什么,即使你使用的是OOP语言,你仍可能以程序方式进行编码。仅研究OO原则定义是无济于事的。我们应该知道如何将这些原则应用于以OO方式设计解决方案。因此,应该具备对象模型,继承,多态,设计模式等方面的基础知识。
如果您不了解Java语言结构和核心API,那么无论你在理论知识方面有多扎实,也都是徒劳的。对于Java,大家应该有很强的实践经验,使用核心API,如java.lang.*,I/O,异常,集合,泛型,线程,JDBC等。当涉及到构建Web应用程序时,没有无论你使用哪个框架,了解servlet和JSP的概念也是至关重要的也是必要的。
理论上谈论写代码时,事情看起来非常简单。我们也可以在理论上非常容易地解决问题,但是,当我们开始落实我们的思路时,我们才认识到问题的深度。持续编码可以让你将了解语言限制,或在编码时设计最佳实践。
技术的道路上其实我们并不孤单。有很多人在研究我们正在使用的技术。虽然在框架上进行简单的概念验证可能不会给您带来真正的挑战,但当您开始在实际项目中使用它时,您将面临各种奇怪的问题,并且在其官方文档中找不到任何解决方案。在开始研究新技术时,最好的第一件事就是订阅相关的技术论坛。无论遇到什么问题,世界上其他人都可能已经面对并找到了解决方案。如果你能回答其他论坛用户提出的问题,那真的很棒。
正如我已经说过的,你并不孤单。全世界有成千上万的热心技术狂热者在博客上发表他们对技术的见解。你还可以从其他博客和开发人员那里获得有关相同技术的宝贵观点。有些人可能会发现特定的框架特点会非常有用,而其他人可能会发现这些特点是愚蠢而毫无意义的,给出了他自己为什么会这样认为的原因。因此,你可以看到使用这些工具的开发人员技术的高低。
通过查看他人博客获得价值的另一种方法是回复/评论有你自己看法和问题的帖子。
一个优秀的开发人员将学习如何使用框架。但是如果你想成为一名优秀的开发人员,你应该研究各种成功的流行框架的源代码,在这些框架中你可以看到框架的内部工作机制以及最佳实践。当以更有效的方式使用框架时,这将有助于广泛使用。
开源软件开发趋势正在大幅增长。当你对如何使用特定框架有了一个好想法时,但它就有可能变得过时,因为一些新的框架会出现在具有超级功能的背景中。然后,你尝试使用当前框架解决的问题可能已经通过具有简单配置的新框架更容易解决。因此,请密切关注一些新兴技术的发展趋势。
随着时间的推移,你可能需要反复写/复制粘贴相同的代码/配置。保留像log4.properties,JDBC配置等那样的配置片段以及StringUtils,ReflectionUtils,DBUtils等实用程序将更有帮助。我知道,它本身不会让你成为杰出的开发者。但是想象一下,一些合作开发人员要求您帮助从一组对象中获取属性值列表,然后您只需使用您的ReflectionUtil并在几分钟内提供解决方案:这将使你更加出色。
熟悉Agile,SCRUM,XP,Waterfall等各种方法。如今,选择开发方法取决于客户端。一些客户更喜欢敏捷,一些客户对瀑布模型感到满意。 所以对各种方法有所了解将是一个真正的优势。
在日常工作中,你可能会学习到新的更好的做事方式,以及最佳实践,构建理念等。记录这些想法并发布到博客,或在整个社区中分享。想象一下,你解决了在做一个简单的POC时发生的一个奇怪的问题并且你在博客上发表了这个问题。也许世界其他地方的开发人员在生产部署的应用程序上面临同样的问题。想想该解决方案对该开发人员的价值。所以,用博客记录你的想法,可能有一天对他人或你自己有帮助!
]]>本文将讨论微服务中ID分配和权衡的七种策略。
ID是领域驱动设计中实体的定义特征。Id一旦在其直接上下文公开并保留,其他组件就可能会使用它。例如,如果服务A通过Id引用了服务B中的实体,则更改实体B的ID将对服务A产生连锁效应。这就是为什么使用多个工具很重要的原因。在这篇博文中,我们将讨论分配ID及其权衡的7种策略。
这是最简单,最直接的选择:您只需生成GUID并将其用作标识符。
这通常意味着依赖数据库服务器生成Id。
这种方法基于上面的方法。生成顺序ID,但保留仅供内部使用。对于外部使用,您使用Skip32对称加密。这将生成一个看似随机的整数。
在这种方法中,生成一个简短但随机的标识符,然后检查它是否唯一。这是URL缩短工具使用的方法,如bitly。您可以通过多种方式生成它,例如使用随机超过62个字符或散列,base62和子字符串。
可以使用已具有唯一标识的实体。例如,所有书籍都应由ISBN唯一标识。这些类型的键也称为自然键。
在此策略中,依靠用户提供ID。最常见的例子是博客文章。例如,在我的博客上,URL来自标题,可以用作此博客文章的唯一ID。如果创建了大量博客帖子并且碰撞的可能性很高,您可以附加一个哈希值以使其唯一(例如:https://tomharrisonjr.com/uuid-or-guid-as-primary-keys-be-小心-7b2aa3dcb439)。
如果您正在与第三方集成,则可以选择重用其分配的标识。这不一定需要在您的公司外部,而是在服务外部。
那么,你应该使用哪一个?当然,这取决于具体情况。我发现自己大部分时间都在使用GUID。如果它需要人类可读,那么可以使用短随机字符串。
您最常使用的方法是什么?为什么?您是否使用了不在此列表中的其他身份生成策略?
]]>在这篇文章中,我们使用Java和Spring Boot框架开发了一个微服务,然后使用DevOps管道将它与Jenkins和Docker一起部署。
在本文中,将使用 Java 和 Spring 框架创建一个简单的微服务,并使用Jenkins和Docker创建一个DevOps管道。
注意:假设读者具有 Java 和 Web 技术的背景。本文不再单独介绍Spring,Jenkins,Java,Git和Docker的。
将按顺序介绍以下几点:
可以使用以下URL从Github克隆微服务应用程序:
https://github.com/Microservices-DevOps/person.git
实体为Person
,包含名称,电子邮件和id。开发一个管理Person
实体的微服务。
package com.myapp.sample.model; |
使用常规CRUD操作测试实体层。然后,我们检查实体是否被持久化,查询和更新
package com.myapp.sample.model; |
持久层由Spring Boot自动管理。 PagingAndSortingRepository
接口是CrudRepository
的扩展,用于提供使用分页和排序抽象检索实体的附加方法。由于使用这些接口自然会使测试覆盖率达到100%,故无需写其他测试方法。
package com.myapp.sample.repositories; |
PersonService
接口包含三个操作:保存,按ID查找,以及查找Repository层支持的多个CRUD操作的所有实例。
package com.myapp.sample.service; |
PersonService
接口通过调用持久层实现的并未添加任何业务服务实现。由于Spring Boot的持久层测试范围已全部覆盖,因此无需单独测试业务业务。
package com.myapp.sample.service; |
通过将调用业务层来间接调用持久层来暴露REST API。
package com.myapp.sample.controller; |
使用Spring Boot自带测试框架测试 REST API 层
package com.myapp.sample.controller; |
以上这些基本上是用于开发微服务的Java代码。
现在看下管理微服务所需的软件。
在本文的例子中,使用Java8。由于Jenkins需要Java8,所以建议使用Java8。
安装最新版本的Git。
安装最新版本的Docker。由于我有一台Windows8机器,所以我使用的是Docker Toolbox for Windows。
使用以下命令安装MySQL 5.7 Docker镜像:
docker pull mysql |
在Docker中使用ip命令获取Docker实例的ip并替换demo程序中resources\application.properties的ip。
使用以下命令安装Jenkins Blue Ocean版本:
docker pull jenkinsci/blueocean |
现在我们来看看用于构建,部署和管理Git存储库的DevOps管道。在我们理解管道之前,在Git Flow上花几分钟是很重要的。
Git Flow是Git的分支模型。它主要由主分支(与生产代码并行),开发分支(开发的主要分支),发布分支(用于开发)和功能分支(供开发人员使用)组成。在开发人员完成代码之后,会为团队负责人创建一个pull请求,以检查并将代码合并到开发中。创建发布分支后,错误修复将进入此分支,并在代码稳定后再次合并以开发和管理。在此模型中,标签是从主分支创建的,用于发布到生产。
可以在此处查看可视化描述:https://datasift.github.io/gitflow/IntroducingGitFlow.html
有必要在Jenkins中创建一个多分支管道。这允许Jenkins自动处理Git Flow。当提供到Git存储库的链接时,Jenkins会自动选择JenkinsFile。将阶段配置为在签入时触发构建时可视化地查看管道。在此示例中,我们使用PMD,CheckStyle和FindBugs检查代码。欢迎您尝试更成熟的工具,如Sonar,代替PMD,CheckStyle和FindBugs。在管道设置中,我们在一个步骤中构建镜像,并在另一个步骤中运行镜像,以便在主分支中发生更改时更新测试环境容器。打tag时,将使用镜像tag名称更新生产环境,如1.0.0。欢迎您尝试将此示例设置为用于生产的不同Jenkins文件和用于生产的Docker文件,这在生成镜像后是必需的。
#!/usr/bin/env groovy |
具体描述参见:https://jenkins.io/doc/tutorials/build-a-multibranch-pipeline-project/
使用Kubernetes进行部署可以改进此示例。但可以使用Docker创建一个完整的管道,但这不是本文的目标。欢迎大家提出更多好方案。
]]>不妨思考下,遇到问题,你通常是怎么解决的?
如果在技术领域没遇到过什么难题(相对的)的话,只能证明你还涉世不深,高手一般都是填过无数的坑、踩过无数的bug的尸体走过来的,一帆风顺走过来的抗击打能力一般也会比较弱,一旦遇到坎就会熄火。
高中时,有个同学解题特别厉害,按我当时的看法,几乎没什么题目可以难倒他。工作多年后,我发现,这是可以训练出来的:将题目中出现的所有要素与解题答案对标起来,找到其间千丝万缕的联系,基本上就可以解答了。
同样,我们也会遇到一些碰到问题就不知所措的人,当然也包括我们自身。一个人的自我教育能力、成长经历、环境影响都对当下解决问题有着潜移默化的影响。
不逃避问题。虽然有时候会很棘手,可以绕过,如果有机会的话,还是要回头重新审视,看能不能找到对应方案,如果你真的有幸可以遇到一个前人都没有遇到的问题,真的可以去买彩票庆祝一下。在一个地方没有解决问题,在后面的职业生涯中它还会重新出现。
面对一个Bug,高手多半是凭猜测去解决问题,我这么说想必你有点不信。”我也猜了,为什么花了半天没猜对?”这就是经验多寡的差别,凭空无端猜测是不能解决问题的。经历的足够多,总结的足够多,思考的足够多,而这些都不会辜负你,他们会在有需要的时候闪现出来,下面要做的就是去验证这个猜测是否正确,如此循环,终究会找到那个”顿悟时刻”。
不要疏忽日志,很多问题的解法暴露在日志中。很多朋友在IDE的console中抛出一段异常后,习惯的clear干净,干净了也找不到应对之法了。有时候日志暴露过多,会让人找不到头绪,其实关键错误一般存在于一堆异常日志的最后一块中,前面的异常多半是这个根上的异常引起的。
遇到大问题时,学会分解。当面对一本大部头书时,你多半没信心读完。而拿着一本上百页的薄册子时完全不用担心读不完的问题。如果将大部头的书,拆解成N个几百页、几个主题来读,读完应该也不是什么难事,而不是永远束之高阁。要解决一个巨大的问题,分解后,就变成一个解决一个小问题就可以把大麻烦解决掉的套路,有”蚕食”想必更贴切一些。
有后盾在手,各种问题都不怕。一是自己的总结,二是网络中可以利用的资源,三是身边的人力资源。当前两个都失效的时候,平时积累的人脉就会发挥作用。许多人的脑容库总比一个人的大,所以有些问题总会有人之前遇到过,有相应的解决方案。从侧面也说明一个问题,如果一个问题自身无法解决,可以适当将风险抛出,寻求帮助,避免造成更坏的影响。
问题一直都有,看你是迎难而上还是畏缩不前?遇水搭桥、逢山开路,解决足够多的问题后,不成专家都难。
]]>实践过Spring Cloud Config的小伙伴可能遇到和我一样的问题,使用了Spring Cloud Config的外部配置的微服务有时会拿不到配置导致启动异常,查看Spring Cloud Config的日志只是说“The local repository is dirty. Resetting it to origin/master.”,但是紧接着打印“Could not reset to remote for master (current ref=refs/remotes/origin/master), remote: https://github.com/haozi4go/cloud-config.git”,对于local repository is dirty是由于Spring Cloud Config将从git上拉取的配置缓存在了/temp目录下,但linux机器会不定时清理这个目录,最终导致各个微服务从配置微服务拿不到配置(如果配置微服务实例很多时,这种问题会很难排查)。对于这个问题可以修改basedir来避免这个问题,如下
spring: |
对于上各问题,虽然修改了配置的Git目录,但也同样不能避免其他情况会损坏Spring Cloud Config的本地Git仓库,为保险起见请设置以下配置
spring: |
如果配置了错误个Git URl,配置微服务不会完整启动,从而使得其他业务微服务从配置微服务拉取配置失败。为了避免这种情况,建议配置clone-on-start为true,从而能及早地在配置微服务启动时就发现错误。
spring: |
使用默认的凭证链或IAM角色(实例配置文件凭证)
配置对于各个微服务都是至关重要的,如果业务微服务无法获取配置,则服务将无法正常启动。
由于配置客户端如果无法从配置微服务获得配置,则会挂起或关闭,可以考虑重写RetryOperationsInterceptor来控制配置客户端重试获取配置。
]]>早期开发只有两个人,考虑微服务之类的都是多余。不过由于受前公司影响,最初就决定了前后端分离的路线,因为不需要考虑SEO的问题,索性就做成了SPA单页应用。多说一句,前后端分离也不一定就不能服务端渲染,例如电商系统或者一些匿名即可访问的系统,加一层薄薄的View层,无论是php还是用Thymeleaf都是不错的选择。
部署架构上,我们使用Nginx代理前端HTML资源,在接收请求时根据路径反向代理到server的8080端口实现业务。
接口按照标准的Restful来定义
/api/
后面,例如 /api/v2
/api/contacts
,也可以嵌套,如/api/groups/1/contacts/100
POST / PUT / DELELE / GET
,这里有一个坑,PUT和PATCH都是更新,但是PUT是全量更新而PATCH是部分更新,前者如果传入的字段是空(未传也视为空)那么也会被更新到数据库中。目前我们虽然是使用PUT但是忽略空字段和未传字段,本质上是一种部分更新,这也带来了一些问题,比如确有置空的业务需要特殊处理。一般来说代码自动执行的都是单元测试(Unit Test),我们之所以叫集成测试是因为测试用例是针对API的,并且包含了数据库的读写,MQ的操作等等,除了外部服务的依赖基本都是符合真实生产场景,相当于把Jmeter的事情直接在Java层面做掉了。这在开发初期为我们提供了非常大的便利性。但值得注意的是,由于数据库以及其他资源的引入,数据准备以及数据清理时要考虑的问题就会更多,例如如何控制并行任务之间的测试数据互不影响等等。
为了让这一套流程可以自动化的运作起来, 引入Jenkins也是理所当然的事情了。
开发人员提交代码进入gerrit中,Jenkins被触发开始编译代码并执行集成测试,完成后生成测试报告,测试通过再由reviewer进行代码review。在单体应用时代这样的CI架构已经足够好用,由于有集成测试的覆盖,在保持API兼容性的前提下进行代码重构都会变得更有信心。
从数据层面看,最简单的方式就是看数据库的表之间是否有比较少的关联。例如最容易分离的一般来说都是用户管理模块。如果从领域驱动设计(DDD)看,其实一个服务就是一个或几个相关联的领域模型,通过少量数据冗余划清服务边界。单个服务内通过领域服务完成多个领域对象协作。当然DDD比较复杂,要求领域对象设计上是充血模型而非贫血模型。从实践角度讲,充血模型对于大部分开发人员来说难度非常高,什么代码应该属于行为,什么属于领域服务,很多时候非常考验人员水平。
服务拆分是一个大工程,往往需要几个对业务以及数据最熟悉的人一起讨论,甚至要考虑到团队结构,最终的效果是服务边界清晰, 没有环形依赖和避免双向依赖。
由于之前的单体服务使用的是spring boot,所以框架自然而的选择了spring cloud。其实个人认为微服务框架不应该限制技术与语言,但生产实践中发现无论dubbo还是spring cloud都具有侵入性,我们在将nodejs应用融入spring cloud体系时就发现了许多问题。也许未来的service mesh才是更合理的发展道路。
这是典型的Spring Cloud的使用方法,该图取自纯洁的微笑公众号
上文说的一旦要融入异构语言的service,那么服务注册,服务发现,服务调用,熔断和限流都需要自己处理。再有关于zuul要多说几句,Sprin Cloud提供的zuul对Netflix版本的做了裁剪,去掉了动态路由功能(Groovy实现),另外一点就是zuul的性能一般,由于采用同步编程模型,对于IO密集型等后台处理时间长的链路非常容易将servlet的线程池占满,所以如果将zuul与主要service放置在同一台物理机上,在流量大的情况下,zuul的资源消耗非常大。实际测试也发现经过zuul与直接调用service的性能损失在30%左右,并发压力大时更为明显。现在spring cloud gateway是pivotal的主推了,支持异步编程模型,后续架构优化也许会采用,或是直接使用Kong这种基于nginx的网关来提供性能。当然同步模型也有优点,编码更简单,后文将会提到使用ThreadLocal如何建立链路跟踪。
经过大半年的改造以及新需求的加入,单体服务被不断拆分,最终形成了10余个微服务,并且搭建了Spark用于BI。初步形成两大体系,微服务架构的在线业务系统(OLTP) + Spark大数据分析系统(OLAP)。数据源从只有Mysql增加到了ES和Hive。多数据源之间的数据同步也是值得一说的话题,但内容太多不在此文赘述。
服务拆分我们采用直接割接的方式,数据表也是整体迁移。因为几次大改造的升级申请了停服,所以步骤相对简单。如果需要不停服升级,那么应该采用先双写再逐步切换的方式保证业务不受影响。
与CI比起来,持续交付(CD)实现更为复杂,在资源不足的情况我们尚未实现CD,只是实现执行了自动化部署。
由于生产环境需要通过跳板机操作,所以我们通过Jenkins生成jar包传输到跳板机,之后再通过Ansible部署到集群。
简单粗暴的部署方式在小规模团队开发时还是够用的,只是需要在部署前保证测试(人工测试 + 自动化测试)到位。
开源的全链路跟踪很多,比如spring cloud sleuth + zipkin,国内有美团的CAT等等。其目的就是当一个请求经过多个服务时,可以通过一个固定值获取整条请求链路的行为日志,基于此可以再进行耗时分析等,衍生出一些性能诊断的功能。不过对于我们而言,首要目的就是trouble shooting,出了问题需要快速定位异常出现在什么服务,整个请求的链路是怎样的。
为了让解决方案轻量,我们在日志中打印RequestId以及TraceId来标记链路。RequestId在gateway生成表示唯一一次请求,TraceId相当于二级路径,一开始与RequestId一样,但进入线程池或者消息队列后,TraceId会增加标记来标识唯一条路径。举个例子,当一次请求会向MQ发送一个消息,那么这个消息可能会被多个消费者消费,此时每个消费线程都会自己生成一个TraceId来标记消费链路。加入TraceId的目的就是为了避免只用RequestId过滤出太多日志。实现如图所示,
简单的说,通过ThreadLocal存放APIRequestContext串联单服务内的所有调用,当跨服务调用时,将APIRequestContext信息转化为Http Header,被调用方获取到Http Header后再次构建APIRequestContext放入ThreadLocal,重复循环保证RequestId和TraceId不丢失即可。如果进入MQ,那么APIRequestContext信息转化为Message Header即可(基于Rabbitmq实现)。
当日志汇总到日志系统后,如果出现问题,只需要捕获发生异常的RequestId或是TraceId即可进行问题定位
经过一年来的使用,基本可以满足绝大多数trouble shooting的场景,一般半小时内即可定位到具体业务。
在容器化之前,采用telegraf + influxdb + grafana的方案。telegraf作为探针收集jvm,system,mysql等资源的信息,写入influxdb,最终通过grafana做数据可视化。spring boot actuator可以配合jolokia暴露jvm的endpoint。整个方案零编码,只需要花时间配置。
因为在做微服务之初就计划了容器化,所以架构并未大动,只是每个服务都会建立一个Dockerfile用于创建docker image
涉及变化的部分包括:
理由下文一一道来。
我们使用的是Redhat的Openshift,可以认为是k8s企业版,其本身就有service的概念。一个service下有多个pod,pod内即是一个可服务单元。service之间互相调用时k8s会提供默认的负载均衡控制,发起调用方只需要写被调用方的serviceId即可。这一点和spring cloud fegin使用ribbon提供的功能如出一辙。也就是说服务治理可以通过k8s来解决,那么为什么要替换呢?其实上文提到了,Spring Cloud技术栈对于异构语言的支持问题,我们有许多BFF(Backend for Frontend)是使用nodejs实现的,这些服务要想融合到Spring Cloud中,服务注册,负载均衡,心跳检查等等都要自己实现。如果以后还有其他语言架构的服务加入进来,这些轮子又要重造。基于此类原因综合考量后,决定采用Openshift所提供的网络能力替换eruka。
由于本地开发和联调过程中依然依赖eruka,所以只在生产上通过配置参数来控制,
eureka.client.enabled
设置为 false
,停止各服务的eureka注册ribbon.eureka.enabled
设置为 false
,让ribbon不从eureka获取服务列表foo.ribbon.listofservers
设置为 http://foo:8080
,那么当一个服务需要使用服务foo的时候,就会直接调用到http://foo:8080
CI的改造主要是多了一部编译docker image并打包到Harbor的过程,部署时会直接从Harbor拉取镜像。另一个就是数据库的升级工具。之前我们使用flyway作为数据库升级工具,当应用启动时自动执行SQL脚本。随着服务实例越来越多,一个服务的多个实例同时升级的情况也时有发生,虽然flyway是通过数据库锁实现了升级过程不会有并发,但会导致被锁服务启动时间变长的问题。从实际升级过程来看,将可能发生的并发升级变为单一进程可能更靠谱。此外后期分库分表的架构也会使随应用启动自动升级数据库变的困难。综合考量,我们将升级任务做了拆分,每个服务都有自己的升级项目并会做容器化。在使用时,作为run once的工具来使用,即docker run -rm的方式。并且后续也支持了设定目标版本的功能,在私有化项目的跨版本升级中起到了非常好的效果。
至于自动部署,由于服务之间存在上下游关系,例如config,eruka等属于基本服务被其他服务依赖,部署也产生了先后顺序。基于Jenkins做pipeline可以很好的解决这个问题。
其实以上的每一点都可以深入的写成一篇文章,微服务的架构演进涉及到开发,测试和运维,要求团队内多工种紧密合作。分治是软件行业解决大系统的不二法门,作为小团队我们并没有盲目追新,而是在发展的过程通过服务化的方式解决问题。从另一方面我们也体会到了微服务对于人的要求,以及对于团队的挑战都比过去要高要大。未来仍需探索,演进仍在路上。
]]>本文介绍SpringBoot如何使用Prometheus配合Grafana监控。
Prometheus是一个根据应用的metrics来进行监控的开源工具。相信很多工程都在使用它来进行监控,有关详细介绍可以查看官网:https://prometheus.io/docs/introduction/overview/。
Grafana是一个开源监控利器,如图所示。
从图中就可以看出来,使用Grafana监控很高大上,提供了很多可视化的图标。
官网地址:https://grafana.com/
在SpringBoot中使用Prometheus其实很简单,不需要配置太多的东西,在pom文件中加入依赖,完整内容如下所示。
|
配置文件中加入配置,这里就只进行一些简单配置,management.metrics.tags.application属性是本文配合Grafana的Dashboard设置的,如下所示:
spring.application.name=springboot_prometheus |
修改启动类,如下所示.
|
SpringBoot项目到这里就配置完成了,启动项目,访问http://localhost:8080/actuator/prometheus, 如图所示,可以看到一些度量指标。
在prometheus配置监控我们的SpringBoot应用,完整配置如下所示。
# my global config |
启动Prometheus,浏览器访问,查看Prometheus页面,如图所示。
点击如图所示位置,可以查看Prometheus监控的应用。
列表中UP的页面为存活的实例,如图所示。
也可以查看很多指数,如下所示。
启动Grafana,配置Prometheus数据源,这里以ID是4701的Doshboard为例(地址:https://grafana.com/dashboards/4701 )如图。
在Grafana内点击如图所示import按钮
在如图所示位置填写4701,然后点击load。
接下来导入Doshboard。
导入后就可以看到我们的SpringBoot项目对应的指标图表了,如图。
源码地址:https://gitee.com/dalaoyang/springboot_learn/tree/master/springboot2_prometheus
]]>转眼间来到了2019年,新年新的开始,来一起看看2018年还没有实现的愿望吧。作为一个SpringCloud的拥趸者,一直在心头惴惴不安的就是如何让SpringCloud的微服务架构一统天下,实现所有服务在微服务架构下的大同。
话说回来,为啥要以网格服务(Service Mesh)方式来整合现有服务呢?如果您正在使用SpringCloud构建自己的微服务架构,您可能会使用Eruka Server作为服务注册中心,使用Eureka Client进行服务注册,最后使用Ribbon进行服务调用(如下图1 服务治理机制),这些组件使用起来都非常简单易用,能使我们迅速搭建起微服务架构的应用。但在公司的架构演变过程中全部在短时间内使用基于SpringCloud进行微服务化显然是不现实,需要我们将现有服务也按照微服务架构的思想进行整合,进而逐渐下线旧服务。还有就是公司部分服务使用非JAVA语言构建(如Python),在短时间将这些服务改造成基于SpringCloud的微服务显然是不现实的,也是不符合架构演进的原则的,或者这些非JAVA服务已经很稳定无需专门进行微服务化的改造,这样同样需要采用微服务架构的思想整合这些服务。
说到这里,您可能会问这该如何整合?如果您研究过Eureka,你可能会直接在现有服务中实现使用Eureka API进行服务注册进而给给其他系统或服务提供服务,当然还需在现有服务中实现一个类似ribbon调用客户端调用所有在Eureka注册中心已经注册的服务,这样一来显示是对现有服务改动太大了,那我们是不是能将服务注册和服务调用从现有服务中抽离出一个代理服务呢?答案是肯定。这就是提出网格服务的根本出发点,此时我们将这个代理服务叫做Sidecar。在服务网格中Sidecar还扩展了重试/超时、监控、追踪等功能。
先来看下Willian Morgan的官宣说法:
服务网格(Service Mesh)是致力于解决服务间通讯的基础设施层。它负责在现代云原生应用程序的复杂服务拓扑来可靠地传递请求。实际上,Service Mesh 通常是通过一组轻量级网络代理(Sidecar proxy),与应用程序代码部署在一起来实现,而无需感知应用程序本身。
如果用一句话来解释什么是 Service Mesh,可以将它比作是应用程序或者说微服务间的 TCP/IP,负责服务之间的网络调用、限流、熔断和监控。对于编写应用程序来说一般无须关心 TCP/IP 这一层(比如通过 HTTP 协议的 RESTful 应用),同样使用 Service Mesh 也就无须关系服务之间的那些原来是通过应用程序或者其他框架实现的事情,比如 Spring Cloud、OSS,现在只要交给 Service Mesh 就可以了。
Service Mesh 作为 sidecar 运行,对应用程序来说是透明,所有应用程序间的流量都会通过它,所以对应用程序流量的控制都可以在 Service Mesh 中实现。
对于整合现有服务,在理解了服务网格(Service Mesh)架构(如图2 服务网格架构)的基础上,我们也可以在现基于SpringCloud的微服务架构中,通过使用服务网格一样的运行方式(即Sidecar)来实现现有服务的注册与调用(即服务治理),如图3 。
在服务网格中,已经有Linkerd、Envoy、Istio、Conduit、nginMesh等开源项目,但这些均不能直接将服务注册到Eureka Server中,所以需要用SpringCloud的方式生成运行Sidecar。
在基于SpringCloud的微服务中,有Spring Cloud Netflix Sidecar,它包含一个简单的http api去获取一个已知服务的所有实例(例如主机和端口)。你也可以通过嵌入的Zuul代理(Zuul中有一个代理功能)对代理的服务进行调用,Zuul从Eureka服务注册中心获取所有的路由记录(route entries)。通过host发现(host lookup)或者Zuul代理可以直接访问Spring Cloud Config。非jvm需要应该实现一个健康检查,Sidecar能够以此来报告给Eureka注册中心该应用是up还是down状态。总之,Sidecar是作为一个代理的服务来间接性的让其他语言可以使用Eureka等相关组件。通过与Zuul的来进行路由的映射,从而可以做到服务的获取,然后可以使用Ribbon,Feign对服务进行消费,以及对Config Server的间接性调用。
为了更好的理解Sidecar,我们在这里单独起一个应用叫作Sidecar,此应用只用作给其他非JVM服务做代理。构建Sidecare应用,需要添加以下依赖,如下
<dependency> |
启用Sidecar,创建一个Spring Boot应用程序,并在在应用主类上加上@EnableSidecar
注解。该注解包含@EnableCircuitBreaker
, @EnableDiscoveryClient
以及@EnableZuulProxy
,将Sidecar和非JVM服务部署在同一台机器上(也可以部署在同一机器上,但为了好管理和理解,最好部署在同一台机器上)。
配置Sidecar,在application.yml中添加sidecar.port
和sidecar.health-uri
。sidecar.port
属性是非jre程序监听的端口号,这就是Sidecar可以正确注册应用到Eureka的原因。sidecar.health-uri
是非jre应用提供的一个对外暴露的可访问uri地址,在该地址对应的接口中需要实现一个模仿Spring Boot健康检查指示器的功能。它需要返回如下的json文档。(注:通过返回一个json,其用status字段来标识你的应用的服务状态,是up还是down,sidecar会将该状态报告给eureka注册中心从而实现你的服务的状态可用情况。简单的说就是用来控制sidecar代理服务的状态!)
server: |
至此,Sidecar应用就准备好了,具体代码可参见 《增加github链接》。
启动Sidecar应用,观察Eureka注册中心,发现Sidecar应用已经注册到注册中心但是状态显示down,这是因为Sidecar应用代理的服务还没有
对于Sidecar来说,如果能准确的代理非JVM服务,则需要实时检查非JVM服务的健康状态并实时将结果同步到Eureka注册中心,以便服务消费方能及时准确地获取到可调用的服务,所以原有非JVM服务需要实现一个简单的健康检查API,具体json结构如下
{ |
接下来我们以Node.js服务作为现有服务来模拟接入SpringCloud微服务的流程。
由于Node.js社区十分活跃,可选的Rest服务框架非常多。比较主流的有express,koa, hapi等,非常轻量易扩展的也有像connect这样的,这里笔者考虑到群众基础和文档丰富度,选择使用express来开发这样一个可以接入Spring Cloud的Rest服务。
var express = require('express'); |
此时启动Node.js服务,再次观察Eureka注册中心,发现之前注册到注册中心的Sidecar应用状态变为UP正常状态了,这可以说明Sidecar确实及时准确的将被代理服务的状态反映到注册中心了。换言之,注册中心显示的Sidecar应用的状态其实是被代理服务的状态。
为了测试Node.js服务的在基于SpringCloud的微服务中的可用性,本文选取微服务中常用的网关作为Node.js服务的调用方来测试Node.js服务的可用性。
pom.xml |
## application.yml |
// InfoController.java |
测试其他服务通过Sidecar调用Node.js服务
通过GET请求请求http://127.0.0.1:8700/express/instance_info ,响应结果为
{ |
此结果说明请求到网关应用(8700端口)的/express/instance_info请求被转发到Sidecar上了,由于Sidecar应用上并没有/instance_info接口,所以该/instance_info接口请求被转发到Node.js服务上了,也就说Node.js服务已经可以被微服务中的其他服务正常调用了。
测试Node.js服务通过Sidecar调用其他服务
再通过GET请求请求 http://127.0.0.1:8000/query/gateway/info ,响应结果为
{ |
此结果表明请求到Node.js服务(8000端口)的/query/gateway/info的请求后,Node.js服务向网关发送了http://localhost:6666/gateway/instance_info (sidecar占用了6666端口)请求获取了网关的实例信息,换言之,Sidecar可以转发Node.js服务请求其他服务的所有请求。
通过以上两个方面的测试,可以看出Sidecar可以双向代理Node.js服务,也就说,Node.js可以被纳入到基于SpringCloud的微服务中提供服务了。 至此,关于Node.js服务以网格服务方式整合到现有基于SpringCloud的微服务架构中的介绍就到一段路了,下面将介绍Python服务整合到基于SpringCloud的微服务架构中。
上一小节,我们对nodejs服务以服务网格的方式整合到现有微服务架构中进行了讲解测试,本小节我们将把Python服务同样以网格服务的方式整合到基于SpringCloud的微服务架构中。
对于Python几年也是非常火的语言,Python不仅在数据处理上应用广泛同时在Web的应用上也很多,所以Python的web框架也是百花齐放,使用上比较简单又强大的框架有Django、Flask、Bottle等,此次讲解测试选择最流行的Django作为基础框架来开发一个可以接入SpringCloud的Rest服务,以便该文章有更多的受众。
具体使用Django生成一个web项目HelloWorld,这里不再赘述,大家可以参考http://www.runoob.com/django/django-first-app.html ,之后准备view.py和urls.py即可。
urls.py
from django.contrib import admin |
view.py
from django.http import HttpResponse,JsonResponse |
运行python manage.py runserver 0.0.0.0:8000命令启动Python服务,同样观察Eureka注册中心,也发现之前注册到注册中心的Sidecar应用状态变为UP正常状态了,这说明Python服务已经接入基于SpringCloud的微服务中,且能对外提供服务。
此步骤与测试Node.js服务使用的测试服务相同即基于JVM的Gateway。
此步骤与测试Node.js服务可用性的操作相投,这里不再赘述。
至此,关于非JVM服务整合到基于Spring Cloud的微服务中就完成了,当然对于老的JVM服务也可以采用这个思路,来尽量少的减少对现有代码的侵入,也是个不错的选择。这样一来,可以使我们所有的现有服务平滑的演进到微服务,最后基于Spring Cloud来一统微服务的天下。
]]>最近在机器上进行clone和pull等操作github仓库均失败
Initialized empty Git repository in /tmp/kael/.git/ |
访问提示链接https://github.com/haozi4go/cloud.git/info/refs ,提示升级客户端
升级git客户端最新版本,但由于CentOS6、CentOS7通过yum安装只能安装1.x版本,所以下面介绍yum安装git-v2.x版本
卸载旧版本(一般为1.7.1)
yum remove git |
安装Install WANDisco repo package
CentOS7:
yum install http://opensource.wandisco.com/centos/7/git/x86_64/wandisco-git-release-7-1.noarch.rpm |
CentOS6:
yum install http://opensource.wandisco.com/centos/6/git/x86_64/wandisco-git-release-6-1.noarch.rpm |
CentOS5:
不适用,可参考http://www.freebuf.com/column/165868.html |
yum install git |
至此git客户端升级完毕。
设置全局TLS版本
git config --global http.sslversion tlsv1 |
如果仍报ssl错误,继续执行以下命令
yum update -y nss curl libcurl openssh |
]]>本文作者:浩子
原文链接: http://blog.chuangzhi8.cn/2018/10/22/GitHub-clone和pull失败-git客户端升级/
版权归作者所有,转载请注明出处
之前系列文章里面完整的代码已经上github,地址在文章最后
傻瓜都能写出计算机可以读懂的代码,只有优秀的程序员才能写出人能读懂的代码!
在我看来,编写简单的函数是一件简单又困难的事情。简单是因为这没有什么技术难点,困难是因为这是一种思维习惯,很难养成,不写个几年代码,很难写出像样的代码。
大部分的程序员写的都是CRUD、一些业务逻辑的代码,谁实现不了?对于我来说,如果业务逻辑的代码评审,需要人来讲每一个代码做了什么,这样的代码就是不合格的,合格的代码写出来应该像人说话那么简单有条理,基本上是业务怎么样描述需求,写出来的代码就是怎么样的。编写出非开发人员都能看懂的代码,才是我们追求的目标。不要以写出了一些非常复杂的代码而沾沾自喜。好的代码应该是看起来平淡无奇觉得很简单自然,而不是看得人云里雾里的觉得很高深很有技术含量。
如果你做好了我前面几篇文章的要求,编写简单的函数就容易的多,如果你觉得我之前说的去掉local,去掉用户参数这些没有什么必要是小题大做,那么我觉得你写不出简单的函数。从个人经验来说,函数编写的建议有以下几点:
参考我之前的帖子,我的编码习惯 - 参数校验和国际化规范,函数参数里面不要出现local,messagesource,request,Response这些参数,第一非常干扰阅读,一堆无关的参数把业务代码都遮掩住了,第二导致你的函数不好测试,如你要构建一个request参数来测试,还是有一定难度的。
这类参数看着灵活方便,但是灵活的同义词(代价)就是复杂,最终的结果是可变数多bug多质量差。就好比刻板的同义词就是严谨,最终的结果就是高质量。千万不要为了偷懒少几行代码,就到处把map,json传来传去。其实定义一个bean也相当简单,加上lombok之后,代码量也没有几行,但代码可读性就不可同日而语了。做过开发的人应该很容易体会,你如果接手一个项目,到处的输入输出都是map的话,request从头传到尾,看到这样的代码你会哭的,我相信你会马上崩溃很快离职的。
还有人说用bean的话后面加字段改起来麻烦,你用map还不是一样要加一个key,不是更加麻烦吗?说到底就是懒!
如果一个项目的所有代码都如下面这样,我是会崩溃的!
/** |
尽量有清晰的输入输出参数,使人一看就知道函数做了啥。举例:
public void updateUser(Map<String, Object> params){ |
上面的函数,看函数定义你只知道更新了用户对象,但你不知道更新了用户的什么信息。建议写成下面这样:
public void updateUserNickName(long userId, String nickname){ |
你就算不看方法名,只看参数就能知道这个函数只更新了nickname一个字段。多好啊!
这点只可意会不可言传。
编写函数的总体指导思想是抽象和封装,你要把代码的逻辑抽象出来封装成为一个函数,以应对将来可能的变化。以后代码逻辑有变更的时候,单独修改和测试这个函数即可。
这一点相当重要,否则你会觉得怎么需求老变?改代码烦死了。
如何识别可能变的地方,多思考一下就知道了,工作久了就知道了。比如,开发初期,业务说只有管理员才可以删除某个对象,你就应该考虑到后面可能除了管理员,其他角色也可能可以删除,或者说对象的创建者也可以删除,这就是将来潜在的变化,你写代码的时候就要埋下伏笔,把是否能删除做成一个函数。后面需求变更的时候,你就只需要改一个函数。
举例,删除配置项的逻辑,判断一下只有是自己创建的配置项才可以删除,一开始代码是这样的:
/** |
这里我会识别一下,是否可以删除这个地方就有可能会变化,很有可能以后管理员就可以删除任何人的,那么这里就抽成一个函数:
/** |
这就是简单的抽象和封装的艺术。看这些代码,参数多么的简单,很容易理解吧。
这一点非常重要,做好了这点,大部分的小的需求变更对程序员的伤害就会降到最低了!毕竟需求变更大部分都是这些小逻辑的变更。
程序猿不招妹子们喜爱的根本原因在于追求了错误的目标:更短、更小、更快。
这个非常重要,当然很难实现,很多人做技术之前都觉得代码都会做单元测试,实际上和业务相关的代码单元测试是很难做的。
我觉得要编写能测试的函数主要有以下几点:
第一不要出现乱七八糟的参数,如参数里面有request,response就不好测试,
第二你要把函数写小一点。如果一个功能你service代码只有一个函数,那么你想做单元测试是很难做到的。我的习惯是尽量写小一点,力求每一个函数都可以单独测试(用junit测试或者main函数测试都没有关系)。这样会节约大量的时间,尤其是代码频繁改动的时候。我们应用重启一次需要15分钟以上。新手可以写一个功能可能需要重启10几次,我可能只需要重启几次,节约的时候的很可观的。
第三你要有单独测试每一个函数的习惯。不要一上来就测试整个功能,应该一行一行代码、一个一个函数测试,有了这个习惯,自然就会写出能测试的小函数。所以说,只有喜欢编码的人才能写出好代码。
如我的编码习惯 - 配置规范 )这篇文章了,我的配置相关代码,都是可以单独测试的,所以配置项的改动不需要测试业务功能,应用都不需要重启。
一句话总结,使用简单参数,不要出现和业务无关的参数,把可能变化的地方都写出独立的函数,力求每一个函数都可以单独测试。
GITHUB地址
所有的代码细节都在已经上了github了,地址 xwjie/PLMCodeTemplate**,欢迎加星。有问题欢迎提出。
]]>工作中少不了要制定各种各样的配置文件,这里和大家分享一下工作中我是如何制定配置文件的,这是个人习惯,结合强大的spring,效果很不错。
=============================需求==========================
如我们现在有一个这样的配置需求,顶层是Server,有port和shutdown2个属性,包含一个service集合,service对象有name一个属性,并包含一个connector集合,connector对象有port和protocol2个属性。
我一上来不会去考虑是用xml还是json还是数据库配置,我会第一步写好对应的配置bean。如上面的需求,就写3个bean。bean和bean之间的包含关系要体现出来。(使用了lombok)
|
然后找一个地方先用代码产生这个bean:
@Configuration |
然后先测试,看看是否ok。为了演示,我就直接在controller里面调用一下
|
测试一下,工作正常
然后进行业务代码编写,等到所有功能测试完毕,就是【开发后期】,再来定义配置文件。中途当然少不了修改格式,字段等各种修改,对于我们来说只是修改bean定义,so easy。
都ok了,再决定使用哪种配置文件。如果是json,我们这样:
==============================JSON===========================
把上面接口调用的json复制下来,报存到配置文件。
json内容
{ |
然后修改config的bean生成的代码为:
import com.fasterxml.jackson.databind.ObjectMapper; |
代码太简洁了,有没有?!
==============================XML===========================
如果使用XML,麻烦一点,我这里使用XStream序列化和反序列化xml。
首先在bean上增加相关注解
@Data |
XMLConfig工具类相关代码:
public class XMLConfig { |
XStream库需要增加以下依赖:
<dependency> |
所以个人爱好,格式推荐json格式配置。
=========================编码习惯=========================
配置文件编码禁忌:
1.读取配置的代码和业务代码耦合在一起!大忌!千万千万不要!
如下,业务代码里面出现了json的配置代码。
public void someServiceCode() { |
2.开发初期就定配置文件
毫无意义,还导致频繁改动!先定义bean,改bean简单多了。我的习惯是转测试前一天才生成配置文件。
=============================重要============================
最主要的思想是,不要直接和配置文件发生关系,一定要有第三者(这里是配置的bean)。你可以说是中间件,中介都行。 否则,一开始说用xml配置,后面说用json配置,再后面说配置放数据库?这算不算需求变更?你们说算不算?算吗?不算吗?何必这么认真呢?只是1,2行代码的问题,这里使用xml还是json,代码修改量是2行。而且改了测试的话,写个main函数或者junit测试即可,不需要测试业务,工程都不用起,你自己算算节约多少时间。
另外,代码里面是使用spring的习惯,没有spring也是一样的,或者配置的bean你不用spring注入,而用工具类获取也是一样,区别不大。
呕心沥血苦口婆心之作,希望对大家有帮助!其他人有好的习惯更加简洁的方式可在作者知乎专栏留言交流,谢谢阅读!
]]>一个项目不可能没有工具类,工具类的初衷是良好的,代码重用,但到了后面工具类越来越乱,有些项目工具类有几十个,看的眼花缭乱,还有不少重复。如何编写出好的工具类,我有几点建议:
就是要定义自己的工具类,尽量不要在业务代码里面直接调用第三方的工具类。这也是解耦的一种体现。如果我们不定义自己的工具类而是直接使用第三方的工具类有2个不好的地方:
不同的人会使用不同的第三方工具库,会比较乱。
将来万一要修改工具类的实现逻辑会很痛苦。
以最简单的字符串判空为例,很多工具库都有 StringUtils工具类,如果我们使用commons的工具类,一开始我们直接使用 StringUtils.isEmpty ,字符串为空或者空串的时候会返回为true,后面业务改动,需要改成如果全部是空格的时候也会返回true,怎么办?我们可以改成使用 StringUtils.isBlank 。看上去很简单,对吧? 如果你有几十个文件都调用了,那我们要改几十个文件,是不是有点恶心?再后面发现,不只是英文空格,如果是全角的空格,也要返回为true,怎么办?StringUtils上的方法已经不能满足我们的需求了,真不好改了。。。
所以我的建议是,一开始就自己定义一个自己项目的StringUtil,里面如果不想自己写实现,可以直接调用commons的方法,如下:
public static boolean isEmpty(String str) { |
后面全部空格也返回true的时候,我们只需要把isEmpty改成isBlank;再后面全部全角空格的时候也返回true的话,我们增加自己的逻辑即可。我们只需要改动和测试一个地方。
在举一个真实一点的例子,如复制对象的属性方法。
一开始,如果我们自己不定义工具类方法,那么我们可以使用 org.springframework.beans.BeanUtils.copyProperties(source, dest)这个工具类来实现,就一行代码,和调用自己的工具类没有什么区别。看上去很OK,对吧?
随着业务发展,我们发现这个方式的性能或者某些特性不符合我们要求,我们需要修改改成 commons-beanutils包里面的方法,org.apache.commons.beanutils.BeanUtils.copyProperties(dest, source) ,这个时候问题来了,第一个问题,它的方法的参数顺序和之前spring的工具类是相反的,改起来非常容易出错!第二个问题,这个方法有异常抛出,必须声明,这个改起来可要命了!结果你发现,一个看上去很小的改动,改了几十个文件,每个改动还得测试一次,风险不是那么得小。有一点小奔溃了,是不是?
等你改完之后测试完了,突然有一天需要改成,复制参数的时候,有些特殊字段需要保留(如对象id)或者需要过滤掉(如密码)不复制,怎么办?这个时候我估计你要崩溃了吧?不要觉得我是凭空想象,编程活久见,你总会遇到的一天!
所以,我们需要定义自己的工具类函数,一开始我定义成这样子。
public void copyAttribute(Object source, Object dest) { |
后面需要修改为commons-beanutis的时候,我们改成这样即可,把参数顺序掉过来,然后处理了一下异常,我使用的是Lombok的SneakyThrows来处理异常,你也可以捕获掉抛出运行时异常,个人喜好。
|
再后面,复制属性的时候需要保留某些字段或者过滤掉某些字段,我们自己参考其他库实现一次即可,只改动价格和测试一个文件一个方法,风险非常可控。
还记得我之前的帖子里说的需求变更吗?你可以认为这算需求变更,但同样的需求变更,我一个小时改完测试,没有任何风险轻轻松松上线,你可能满头大汗加班加点还担心出问题。。。
上面那点隐藏实现,说到底是封装/解耦的思想,而现在说的这点是抽象的思想,做好了这点,我们就能编写出看上去很专业的工具类。这点很好理解,但是我们容易忽略。
举例,假设我们写了一个判断arraylist是否为空的函数,一开始是这样的。
public static boolean isEmpty(ArrayList<?> list) { |
这个时候,我们需要思考一下参数的类型能不能使用父类。我们看到我们只用了size方法,我们可以知道size方法再list接口上有,于是我们修改成这样。
public static boolean isEmpty(List<?> list) { |
后面发现,size方法再list的父类/接口Collection上也有,那么我们可以修改为最终这样。
public static boolean isEmpty(Collection<?> list) { |
到了这部,Collection没有父类/接口有size方法了,修改就结束了。最后我们需要把参数名字改一下,不要再使用list。改完后,所有实现了Collection都对象都可以用,最终版本如下:
public static boolean isEmpty(Collection<?> collection) { |
是不是看上去通用多了 ,看上去也专业多了?上面的string相关的工具类方法,使用相同的思路,我们最终修改一下,把参数类类型由String修改为CharSequence,参数名str修改为cs。如下:
public static boolean isEmpty(CharSequence cs) {
return org.apache.commons.lang3.StringUtils.isEmpty(cs);
}
思路和方法很简单,但效果很好,写出来的工具类也显得很专业!总结一下,思路是抽象的思想,主要是修改参数类型,方法就是往上找父类/接口,一直找到顶为止,记得修改参数名。
开发过的兄弟都知道,有一些工具库,有一堆的重载函数,调用起来非常方便,经常能直接调用,不需要做参数转换。这些是怎么样编写出来的呢?我们举例说明。
现在需要编写一个方法,输入是一个utf-8格式的文件的文件名,把里面内容输出到一个list。我们刚刚开始编写的时候,是这个样子的
public static List<String> readFile2List(String filename) throws IOException { |
我们先实现,实现完之后我们做第一个修改,很明显,utf-8格式是很可能要改的,所以我们先把它做为参数提取出去,方法一拆为二,就变成这样。
public static List<String> readFile2List(String filename) throws IOException { |
多了一个方法,直接调用之前的方法主体,主要的代码还是只有一份,之前的调用地方不需要做任何修改!可以放心修改。
然后我们在看里面的实现,下面这2行代码里面,String类型的filename会变化为File类型,然后在变化为FileInputStream 类型之后才使用。
File file = new File(filename); |
这里我们就应该想到,用户可能直接传如File类型,也可能直接传入FileInputStream类型,我们应该都需要支持,而不需要用户自己做类型的处理!在结合上一点的使用父类,把FileInputStream改成父类InputStream,我们最终的方法组如下:
package plm.common.utils; |
怎么样?6个方法,实际上代码主体只有一份,但提供各种类型的入参,调用起来很方便。开发组长编写的时候,多费一点点时间,就能写来看上去很专业调用起来很方便的代码。如果开发组长不写好,开发人员发现现有的方法只能传String,她要传的是InputStream,她又不敢改原来的代码,就会copy一份然后修改一下,就多了一份重复代码。代码就是这样烂下去了。。。
关键点,多想一步,根据参数变化编写各种类型的入参函数,需要保证函数主要代码只有一份。
工具类的一个问题就是容易泛滥,主要原因是开发人员找不到自己要用的方法,就自己写一个,开发人员很难记住类名,你也不可能天天代码评审。
所以要让开发人员容易找到,我们可以使用静态引入,在Eclipse里面这样导入:
这样,任何地方开发人员只要一敲就可以出来,然后再约定一下项目组方法名规范,这样工具类的使用就会简单很多!
这点是我的习惯,我习惯把和业务无关的代码放到独立的工程或者目录,在物理上要分开,专人维护。不是所有人都有能力写工具类,独立存放专门维护,专门的权限控制有助于保证代码的纯洁和质量。这样普通的开发人员就不会随意修改。
例如我的范例工程里面,专门建立了一个source目录存放框架代码,工具类也在里面,这里的代码,只有我一个人会去修改:
几乎所有人都知道面向对象的思想有抽象封装,但几个人真正能做到,其实有心的话,处处都能体现出这些思想。编写工具类的时候需要注意参数的优化,而且大型项目里面不要在业务代码里面直接调用第三方的工具类,然后就是多想一步多走一步,考虑各种类型的入参,这样你也能编写出专业灵活的工具类!
Github:晓风轻的代码模板:https://github.com/xwjie/PLMCodeTemplate ,欢迎加星fork。
]]>对于大型IT系统,最怕的事情第一是系统出现了异常我不知道,等问题闹大了用户投诉了才知道出问题了。第二就是出了问题之后无法找到出错原因。针对这2个问题,说说我们项目组是怎么样规定异常处理的。
再次声明我的观点,我这系列贴里面,没有什么技术点,都是一些编程的经验之谈,而且是建立在项目背景是大部分代码都是简单的CRUD、开发人员流动大水平一般的情况下。希望读者的重点不要再关注技术点。大部分工作中不需要什么技术,你只要把代码写好,足够你轻松面对!
言归正传,说回第一个问题,系统出异常了我不知道,等问题闹大了用户投诉了才知道。这个问题出现非常多,而且非常严重。我不知道其他公司有没有这种场景,对我们公司而言,经常会出现用户反馈、投诉过来说某个功能不可用,开发人员定位分析之后,才发现之前的某一步出错了。公司业务流程非常复杂,和周边系统一堆集成,一堆的后台队列任务,任何一部都可能出问题。
举几个今年真实的案例:
1.某系统销户无法成功,最后定位发现前段时间ldap密码修改没有更新
2.某个流程失败,最后发现集成的系统新增加了NAS盘,防火墙不通无法访问导致报错。
3.某个功能无法使用,查看日志发现后台定时任务已经停了好几天。
针对这些功能,在流程上当然可以采取相对的策略来保证,但从开发的角度来说,任何规定都无法保证一定不会发生错误,老虎也有打盹的时候,我只相信代码。
贴一段非常常见的代码,大家觉得这段代码有没有问题?
在我看来,这段代码很多时候问题特别大!
1.丢掉了异常。异常就算打印了堆栈,也不会有人去看的!除非用户告诉你出问题了,你才会去找日志!所以,看着好像很严谨的代码,其实作用并不大
2.异常处理再加上框框2处的空判断,天衣无缝的避开了所有正确答案。本来需要更新文档,结果什么错误没有报,什么也没有做。你后台就算打了日志堆栈又怎么样?
所以,我对开发人员的要求就是,绝大部分场景,不允许捕获异常,不要乱加空判断。只有明显不需要关心的异常,如关闭资源的时候的io异常,可以捕获然后什么都不干,其他时候,不允许捕获异常,都抛出去,到controller处理。空判断大部分时候不需要,你如果写了空判断,你就必须测试为空和不为空二种场景,要么就不要写空判断。
强调,有些空判断是要的,如:参数是用户输入的情况下。但是,大部分场景是不需要的(我们的IT系统里面,一半以上不需要),如参数是其它系统传过来,或者其他地方获取的传过来的,99.99%都不会为空,你判断来干嘛?就抛一个空指针到前台怎么啦?何况基本上不会出现。
新手最容易犯的错误,到处捕获异常,到处加空判断,自以为写出了“健壮”的代码,实际上完全相反。导致的问题,第一代码可读性很差,你如果工作了看到一半代码是try-catch和空判断你会同意我的观点的,第二更加重要的掩盖了很多错误,如上面图片的例子!日志是不会有人看的,我们的目的是尽早让错误抛出来,还有,你加了空判断,那你测试过为空的场景吗?
web请求上的异常,不允许开发人员捕获,直接抛到前台,会有controller处理!见我的编码习惯 - Controller规范)
所以上面的代码,我来写的话是这样的,清晰明了。
另外一种后台定时任务队列的异常,其实思路是一样的,有个统一的地方处理异常,里面的代码同样不准捕获异常!然后异常的时候邮件通知到我和开发人员,开发组长必须知道后台的任何异常,不要等用户投诉了才知道系统出问题了。
另外,开发组长需要自己定义好系统里面的异常,其实能定义的没有几种,太细了很难落地,来,异常不要继承Exception,而是继承RuntimeException,否则到时候从头改到尾就为了加个异常声明你就觉得很无聊。
总结:
1.开发组长定义好异常,异常继承RuntimeException。
2.不允许开发人员捕获异常。(异常上对开发人员就这点要求!异常都抛出到controller上用AOP处理)
3.后台(如队列等)异常一定要有通知机制,要第一时间知道异常。
4.少加空判断,加了空判断就要测试为空的场景!
这篇文章,我估计一定有很多争议,这些规则都和常见的认识相反,我在公司里面推广和写贴分享的时候也有人反对。但是,你要知道你遇到的是什么问题,要解决的是什么问题?我遇到是很多异常本来很简单,但由于一堆健壮的try-catch和空判断,导致问题发现很晚,可能很小一个问题最后变成了一个大事件,在一些IT系统里面,尤其常见。大家不要理解为不能加空判断,大家见仁见智吧。反正我是这样写代码的,我发现效果很好,我很少花时间在调试代码和改bug上,更加不会出现前台返回成功,后台有异常什么也没有做的场景。
最后对新手说一句,不要养成到处try-catch和加空判断的恶习,你这样会掩盖掉很多错误,给人埋很多坑的!
]]>今天我们说说参数校验和国际化,这些代码没有什么技术含量,却大量充斥在业务代码上,很可能业务代码只有几行,参数校验代码却有十几行,非常影响代码阅读,所以很有必要把这块的代码量减下去。
今天的目的主要是把之前例子里面的和业务无关的国际化参数隐藏掉,以及如何封装好校验函数。
今天累,少说话多贴代码,先看对比图,修改前:
service
修改后:
services
Controll的非业务代码如何去掉参考我的编码习惯 - Controller规范,下面说说去掉Local参数。
强调一下:业务代码里面不要出现和业务无关的东西,如local,MessageSource。
去掉国际化参数还是使用的技术还是ThreadLocal。国际化信息可以放好几个地方,但建议不要放在每一个url上,除了比较low还容易出很多其他问题。这里演示的是放在cookie上面的例子:
UserUtil
public class UserUtil { |
CheckUtil,这里需要得到用户的语言
package plm.common.utils; |
这里有几个小技术点:
工具类里面使用spring的bean,使用了MethodInvokingFactoryBean的静态方法注入:
<!-- 国际化 --> |
server里面调用的使用没有出现类名
这里使用的jdk的import static 特性,可以在ide上配置,请自行google。
import static plm.common.utils.CheckUtil.*; |
还有一小点注意,我建议参数非法的时候,把值打印出来,否则你又要浪费时间看是没有传呢还是传错了,时间就是这样一点点浪费的。
check(id > 0L, "id.error", id); // 当前非法的id也传入提示出去 |
另外有些项目用valid来校验,从我实际接触来看,用的不多,可能是有短木板吧。如果你的项目valid就能满足,那就更加好了,不需要看了。但是大部分场景,校验比例子复杂N多,提示也千变万化,所以我们还是自己调用函数校验。
做了这几步之后,代码会漂亮很多,记住,代码最主要的不是性能,而是可读性,有了可读性才有才维护性。而去掉无关的代码后的代码,和之前的代码对比一下,自己看吧。
还有人说代码要注释率到多少(我们公司有段时间工具扫描要求注释率到30%以上),依我看来,大部分业务代码这么简单,你把代码写成我例子那样,还需要什么注释?注释是画蛇添足。
周末再更新一下剩余的规范。敬请期待。
]]>开发中日志这个问题,每个公司都强调,也制定了一大堆规范,但根据实际情况看,效果不是很明显,主要是这个东西不好测试和考核,没有日志功能一样跑啊。
但编程活久见,开发久了,总会遇到“这个问题生产环境上能重现,但是没有日志,业务很复杂,不知道哪一步出错了?” 这个时候,怎么办? 还能怎么办,发个版本,就是把所有地方加上日志,没有任何新功能,然后在让用户重现一遍,拿下日志来看,哦,原来是这个问题。
有没有很熟悉的感觉?
还有一种情况,我们系统有3*5=15个节点,出了问题找日志真是痛苦,一个一个机器翻,N分钟后终于找到了,找到了后发现好多相似日志,一个一个排查;日志有了,发现逻辑很复杂,不知道走到那个分支,只能根据逻辑分析,半天过去了,终于找到了原因。。。一个问题定位就过去了2个小时,变更时间过去了一半。。。
所以我对日志的最少有以下2点要求:
1.能找到那个机器
2.能找到用户做了什么
针对第一点,我修改了一下nginx的配置文件,让返回头里面返回是那个机器处理的。
nginx的基本配置,大家查阅一下资料就知道。简单配置如下(生产环境比这个完善)
效果如图,返回了处理的节点:
第二点,要知道用户做了什么。用户信息是很重要的一个信息,能帮助海量日志里面能快速找到目标日志。一开始要求开发人员打印的时候带上用户,但是发现这个落地不容易,开发人员打印日志都经常忘记,更加不用说日志上加上用户信息,我也不可能天天看代码。所以找了一下log4j的配置,果然log4j有个叫MDC(Mapped Diagnostic Context)的类(技术上使用了ThreadLocal实现,重点技术)。具体使用方法请自行查询。具体使用如下:
filter中得到用户信息,并放入MDC,记住filter后要清理掉(因为tomcat线程池线程重用的原因)。
用户信息放入MDC:
log4j配置,增加用户信息变量:
我做好上面2步后,对开发人员的日志只有3点要求:
1.修改(包括新增)操作必须打印日志
大部分问题都是修改导致的。数据修改必须有据可查。
2.条件分支必须打印条件值,重要参数必须打印
尤其是分支条件的参数,打印后就不用分析和猜测走那个分支了,很重要!如下面代码里面的userType,一定要打印值,因为他决定了代码走那个分支。
3.数据量大的时候需要打印数据量
前后打印日志和最后的数据量,主要用于分析性能,能从日志中知道查询了多少数据用了多久。这点是建议。自己视情况而决定是否打印,我一般建议打印。
加上我的编码习惯 - Controller规范 这篇文章的AOP,最后的日志如下:
其实日志的级别我到不是很关注,还没有到关注这步到时候。开发组长需要做好后勤工作(前面2步),然后制定简单规则,规则太多太能落实了。
日志这个东西,更多是靠自觉,项目组这么多人,我也不可能一个一个给大家看代码,然后叫你加日志。我分析了一下,为什么有些人没有打印日志的习惯,说了多次都改不过来。我建议大家养成下面的习惯,这样你的日志就会改善多了!
1.不要依赖debug,多依赖日志。
别人面对对象编程,你面对debug编程。有些人无论什么语言,最后都变成了面对debug编程。哈哈。这个习惯非常非常不好!debug会让你写代码的时候偷懒不打日志,而且很浪费时间。改掉这个恶习。
2.代码开发测试完成之后不要急着提交,先跑一遍看看日志是否看得懂。
日志是给人看的,只要热爱编程的人才能成为合格程序员,不要匆匆忙忙写完功能测试ok就提交代码,日志也是功能的一部分。要有精益求精的工匠精神!
日志规范想不到写了这么多,不容易啊。觉得有帮助请点赞加关注,其他规范敬请期待!更多规定请看程序员你为什么这么累?最后链接。谢谢!
]]>导读: 程序员你为什么这么累?
第一篇文章中,我贴了2段代码,第一个是原生态的,第2段是我指定了接口定义规范,使用AOP技术之后最终交付的代码,从15行到一行,自己感受一下。今天来说说大家关注的AOP如何实现。
先说说Controller规范,主要的内容是就是接口定义里面的内容,你只要遵循里面的规范,controller就问题不大,除了这些,还有另外的几点:
原因见我的接口定义这个贴。没有统一格式,AOP无法玩。
1.ResultBean/PageResultBean是controller专用的,不允许往后传!
2.Controller做参数格式的转换,不允许把json,map这类对象传到services去,也不允许services返回json、map。
一般情况下!写过代码都知道,map,json这种格式灵活,但是可读性差,如果放业务数据,每次阅读起来都比较困难。定义一个bean看着工作量多了,但代码清晰多了。
主要是可读性问题。一般情况下。
1.不需要打印日志
日志在AOP里面会打印,而且我的建议是大部分日志在Services这层打印。
规范里面大部分是 不要做的项多,要做的比较少,落地比较容易。
ResultBean定义带泛型,使用了lombok。
AOP代码,主要就是打印日志和捕获异常,异常要区分已知异常和未知异常,其中未知的异常是我们重点关注的,可以做一些邮件通知啥的,已知异常可以再细分一下,可以不同的异常返回不同的返回码:
public class ControllerAOP { |
AOP配置:(关于用java代码还是xml配置,这里我倾向于xml配置,因为这个会不定期改动)
<!-- aop --> |
现在知道为什么要返回统一的一个ResultBean了:
贴一个简单的controller(左边的箭头表示AOP拦截了)。请对比程序员你为什么这么累?里面原来的代码查看,没有对比就没有伤害。
最后说一句,先有统一的接口定义规范,然后有AOP实现。先有思想再有技术。技术不是关键,AOP技术也很简单,这个帖子的关键点不是技术,而是习惯和思想,不要捡了芝麻丢了西瓜。网络上讲技术的贴多,讲习惯、风格的少,这些都是我工作多年的行之有效的经验之谈,望有缘人珍惜。
觉得有用请点赞加关注,接下来再继续其他规范。敬请期待。
]]>