diff --git a/README.md b/README.md index bf2ffa4..d6eefc8 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,18 @@ 《Java工程师修炼之道》 +[](https://api.gitsponsors.com/api/badge/link?p=ALQJxZauryyAQlf0IzyvqEw0lM+sJ1NbMZq6Pj0yXVBdL8fXZrXF01cpSpV4vg2m0zTQIr+meODxd3moQ+dRKqqP59cdgJ3A/y5/peB19b5FxkRxaENv1mQpNeBrd7ODD2h13/zbtoA1S3Bec3W/Qw==) -- +购买纸质书籍可至: + -- [书籍](book) +- [在线书籍](https://rowkey-books.gitbook.io/pragmatic-java-engineer/) - [代码](source) - [勘误](https://github.com/superhj1987/pragmatic-java-engineer/wiki/Mistakes) -**已开源章节** - -> - [1.1 后端基础设施](book/chapter1-servertech/server-basic.md) -> - [1.2 Java后端技术概览](book/chapter1-servertech/server-tech-tree.md) -> - [1.3 如何学习后端技术](book/chapter1-servertech/how-to-study.md) -> - [2.1 项目构建](book/chapter2-project/build.md) -> - [2.2 代码版本控制](book/chapter2-project/vcs.md) -> - [2.3 代码质量保证](book/chapter2-project/quality.md) -> - [5.3 缓存](book/chapter5-datastore/cache.md) -> - [8.1 调优准备](book/chapter8-profile/ready.md) -> - [附录A: 代码构建常用命令](book/appendix/build-cmd.md) -> - [附录B: Git常用命令](book/appendix/git-usage.md) -> - [附录E: Java调优常用命令](book/appendix/java-profile.md) -> - [附录F 如何应对在线故障](book/appendix/online-debug.md) -> - [附录G 架构简明指南](book/appendix/arch-usage.md) +### 内容介绍 -[**购买链接**](https://item.jd.com/12325207.html) +[**前言**](book/README.md) ### 后续计划 @@ -42,70 +31,6 @@ - 补充RxJava的使用在Java开发利器中。 - 补充Java10和Kotlin的部分到Java新版本特性。 -**也欢迎大家提交内容,以丰富此书。^_^** - -### 内容介绍 - -见 [**前言**](book/README.md) - -### 推荐 - ->2013年,我和本书作者的接触是从基于网易的一个大型互联网应用合作开始的,我见证了从第一行代码到整个系统服务于亿级用户的过程,并且相信这种经历对开发者来说是一笔巨大的财富,其中大量的开发和实战经验都会在本书中得到充分的体现,相信读者能从书中直接领略到丰富的实战知识。在与本书作者的合作过程中,其对Java技术的热爱与追求孜孜不倦,对问题刨根问底,直到理解透彻、灵活应用,这些都令我印象深刻。这些年,我与本书作者一直保持沟通交流、相互学习,他将近十年的实战经验沉底于本书以实现对后端技术的探索、布道,非常值得开发者与近高窗卧听秋。 -> ->后端技术涉及内容非常广泛,Java语言也是互联网开发行业使用的主流语言,相信后续也将继续流行很长一段时间,而本书作者也一直从事Java后端开发工作。在本书中作者比较系统地从总体上描述了后端技术相关的理论知识,包括基础设施、网关服务及框架选型等基本原则,然后以实际经验进行示例说明,接着详细梳理了Java的后端技术,相信读者读完本书后会更全面地理解后端技术。互联网的业务建设需要不同角色的开发者共同协作完成,因此,系统工程化是开发者首先要共同遵守的规范或约定,包括代码规范、版本管理和代码质量检查等。 -> ->开发框架的选型进一步地为工程化提供了基础,也能加速推进互联网开发,尽管是否重复造轮子是一个恒久的话题,但是没有永远的银弹,只要在合适的时间里根据团队的能力选择合适的技术框架就好。一般来讲,目前常用的框架包括基本的依赖注入、AOP、事务管理、连接池管理、数据操作、日志服务等,在众多的框架中,本书作者选用目前在Java领域使用最广泛的Spring做深入的分析,详细地说明各组件的基础知识、基本原理和实际使用案例,最难得的是把较多开发者遇到的坑都用真实的示例进行了说明,可以帮助开发者快速地跳过这些伤心地带,同时也把最佳实践画龙点睛地带给开发者。 -> ->数据存储无疑是所有系统应用中非常重要的一环,应用的场景用例也和数据库的选型有极其重要的关系,开发者选择关系型数据库还是非关系型数据库是需要根据软件成本与人力成本来进行权衡的,比如是选择MySQL、Oracle等开源或商业的数据库。本书重点从数据库的基础知识、索引和表优化等方面以详尽的示例为更好地选择数据库的存储类型提供了更多的知识。 - ->早期的关系型数据库一般能满足数据达到一定规模的企业的需求,而在互联网业务领域,特别是移动互联网领域内的元数据或者日志数据等,达到亿数量级别是很常见的,这时通常使用非关系型数据库,在非关系型数据库里使用非常多的有MongoDB、HBase等分布式数据库系统。作者在自身的企业开发实践中,得到了大量的使用经验和最佳实践。为了加速后端应用,缓存热数据是加速业务、提高业务性能、提升用户体验的重要手段,通过使用本地缓存、远程缓存进行数据加速、数据预热或提高数据的命中率,是开发者在应用开发的过程中常会遇到的场景。 -> ->“路漫漫其修远兮,吾将上下而求索”,后端技术每年都在不断发展,所用技术也有变化,近些年Java语言的发展速度不那么快了,但是总体是在不断前进发展的,本书作者带领的团队一直深耕此领域并希望通过本书为技术开发人员带来更多帮助。 -> ->-- **尧飘海,网易云基础服务(蜂巢)首席架构师** - ---- - ->Hey!新来的读者,为了吸引你的注意力我真是煞费苦心,但最终还是没能写出一句特别吸引眼球的话来,毕竟写序的我不是标题党出生。此刻我真的非常能理解你拿到新书之后那渴望知识的心情,所以你恨不得一个字的“序”也不要看到,直接到达“最有价值”的知识点。但作为一名资深转业码农(对!你没看错,是“转业”,不是“专业”)还是想说一句,你先看完序,5分钟后到达知识的战场,会更稳! -> ->相信你已经在看“序”了,那么我们来说点正经事。 -> ->你的知识体系的养成有3个关键阶段:看山是山、看山不是山、看山还是山。本书的适用人群是“看山不是山”的那些人,如果你恰好处于这个阶段,恭喜你!书钱没白花。 -> ->Java是一门非常容易入门的语言,初学者经过初期的学习之后基本能掌握DEMO级别的编程应用。相信读者你已经度过了这个阶段,但是Java庞大的体系可能会把你绕晕,又或者你还没看到Java的生态系统有多么复杂。此时,你需要本书。从事程序员这个工作,到比较高阶的时候,其实是不挑语言的,语言只是工具,而你可以在纷繁复杂中游刃有余。但几乎每一位高手都是先深入一个领域,再横向发展的。你可以不用着急后续的横向发展,先坚定自己学习Java的信心!因为,从广泛的应用场景、顶级的开源生态、未来可期的薪水和职位来说,Java都是非常不错的选择。 -> ->敲黑板,画重点!下面来解释一下,为什么本书面向的是“看山不是山”的人群。在度过Java的入门期之后,会有一个烦恼,那就是面对Java这么庞大的体系,我们究竟应该学习什么?选择方向,往往比努力更重要!是使用J2SE编写桌面程序?是使用J2ME编写嵌入式应用?还是使用J2EE编写企业级应用?这些是我们那个泛黄的年代特有的烦恼。而现在的烦恼可能是学Android?还是学Java后端?即便大方向你已经十分坚定,而且选择了Java后端编程,但因为复杂的知识体系和Google发布的各种教程文档,眼前看到的已经不再是清晰的山脉,而是一片迷雾。此时,你需要本书,因为它给你指明了努力的方向。 -> ->本书的结构、阐述的方式和大部分的“指南”书籍有较大的区别,本书是以笔记和要点的形式进行呈现的,用现在的话说就是捞干货。本书涵盖的知识,是以现代工程实践中的实际案例出发来组织的,所以知识点范围非常广泛,每一个点都对最关键的“Best Practice”简明扼要地进行了说明。你在阅读本书的时候需要一些相关经验,不然无法跟上作者的节奏,建议在有一定的知识准备后再阅读本书,这样你会受益匪浅。从另外一个角度看,在你有了一定的基础积累之后,本书可以帮助你全面地了解一个现代化的最先进的工程实践是怎样的。本书讲述了目前行业中最常用的,经过了实践的工程方案,这将是你快速进阶的最佳指引。 -> ->-- **孙建,随身云(中华万年历)联合创始人&CEO** - ---- - ->扎实的基础理论知识是内功底子,丰富的实践经验是招式。如本书作者所说,精妙的招式决定了你的武功下限,而深厚的内功底蕴会承载你所能企及的高度。那么,在后端技术栈中,内功与招式之间如何去关联起来,本书作者以其多年的钻研与实践结合心得,通过本书为你一一梳理。 -> ->-- **阙杭宁,网易云信CTO** - ---- - ->作者是一位技术人,有多年的Java技术积累,是极少数真正热爱技术的人。在随身云架构师的工作让他有机会站在更高的层次进行系统架构的工作,这些实践经验和平时感悟都沉淀在作者的著作和博客中,相信每位Java工程师都能从中获取帮助。 -> ->-- **秦绪震,十露盘科技联合创始人,技术负责人** - ---- - ->本书作者根据自身多年的JAVA后台开发经验, 提纲挈领的总结JAVA后台开发的各个关键技术点,这些知识点都是一个合格的JAVA工程师必须掌握的技能。它既可以作为新人的技术学习指南,也可以帮助老手对于自己的知识面进行查漏补缺,是一本非常好的技术指南。 -> ->-- **饶洵(蜚天),阿里巴巴技术专家** - ---- +## Star History ->作为一个在后端摸爬多年的Java开发工程师,这本书让我温故而知新。书中介绍的Java相关的知识技能树,不仅涵盖了我个人多年的Java开发技术知识点,也对我所陌生的一些知识点进行了详解,让我突然有一种继续学习的冲动。 -> ->一个Java开发工程师的成长,不仅要对Java语言及其特性有深层次的理解,也需要掌握与Java相关的框架、生态及后端开发知识。这本书正是将后端开发工程师需要掌握的技能做了总结,对于提高开发技能有很好的指导作用。 -> ->我推荐这本书,对于具有一定Java基础和后端开发知识的读者来说,该书不仅具有仔细学习的价值,同时也是一本可以经常翻阅的工具书籍,对于Java开发工程师的成长和进阶有很大的指导作用。 -> -> 一本好的技术书籍,不仅要仔细阅读、学习理解,还需要进行较多的实践,将所看所学进行应用,通过不断地实践,加深知识点印象,从而形成永久的记忆和技能。希望各位读者能够通过掌握书中的知识和技能,逐步成长为技术骨干和专家,从而创造更多的技术输出、产品输出,创造更多的财富。 -> -> -- **张小川,网易考拉海购架构师,供应链技术主管** +[![Star History Chart](https://api.star-history.com/svg?repos=superhj1987/pragmatic-java-engineer&type=Date)](https://star-history.com/#superhj1987/pragmatic-java-engineer&Date) diff --git a/book/README.md b/book/README.md index 24fcac5..4e60143 100644 --- a/book/README.md +++ b/book/README.md @@ -2,102 +2,38 @@ 目前互联网行业如火如荼,进入这个行业的技术人员也越来越多。对于研发来说,从工程角度主要分为:前端工程师、客户端工程师(又分为iOS和Android工程师)、后端工程师、算法工程师等职位。本书所说的Java工程师指的是以Java做为主要开发语言的后端工程师。 -从2008年还未毕业时做一些小的项目至今做后端开发已经有差不多十年时间。经历过刚学Java时的迷茫,第一次写出Java程序时的激动,第一次写出一个Web系统的醍醐灌顶,一直到接触到Java更底层的东西,对Java有了一个系统的认识,对后端技术体系有了一个宏观的感受。这期间,用过各式各样的编程语言,尝试过各种开源软件,挖过各种坑,也填过各种坑。就单单针对后端的技术来说,自己的这些知识体系,还是觉得是有一定价值的。 +本书会针对Java后端开发工作中经常用到的关键技能点去做阐述,会尽量覆盖在实际工作中需要的所有技能点。但由于很多技能点并非一两篇文章就能讲述完成的,本书仅仅是做一些实践性的经验总结和阐述,更加详细和深入地学习则需要参考专门的书籍或者官方文档。 -此外,还记得笔者毕业后进入第一家公司时,入职培训的课程对于自己来说虽然不难,但确实让自己有种恍然大悟的感觉。业界的最佳实践和自己在学校里学到的、使用到的差别还是非常大的。直到后来加入当前的这家公司,做过一系列后端技术的培训课程,并且在校招的笔试和面试过程中,深刻体会到了学校中的知识与业界脱节之严重,在平时的社招中也遇到很多对后端技术缺乏系统性认识、技能点不足的工程师们,也经常被人问起如何学习Java后端技术。于是就打算写一下目前后端工程师一些比较主流前沿的技术以及实际工作中会用到的一些技能并串联起来,给刚上大学以后打算以Java后端为职业的学生、刚毕业入职的应届生以及初学者们一些入门的指引,避免走一些弯路,也给一些有经验的工程师们提供一个参考手册将零散的知识点串起来,减少在解决某些实际问题时无头绪搜索带来的时间成本,同时也是对自己的一个阶段性总结和查漏补缺。需要注意的一点是,像数据结构、计算机网络等计算机科学基础知识以及JavaSE基本用法,笔者认为是从事程序开发工作的Java工程师应该必备的知识点,因此并不包括在内。 +> - 《Effective Java(第3版)》:此书讲解了Java的一些高级特性和技巧。最新的第三本加入了Java8新语言特性部分。 -本书会针对Java后端开发工作中经常用到的关键技能点去做阐述,会尽量覆盖在实际工作中需要的所有技能点。但由于很多技能点并非一两篇文章就能讲述完成的,本书仅仅是做一些实践性的经验总结和阐述,更加详细和深入地学习则需要参考专门的书籍或者官方文档。因此,如果是对内容深度有要求的读者,那么本书并不适合。 - -本书的大部分内容都来自笔者的博客以及平时工作、学习中的一些自我总结和笔记,记录了笔者进入这个行业以来的一些经验教训和思考,也是自己平时工作时会经常查阅的参考手册。 - -## 读者对象 - -- 未入门或者刚入门的Java工程师 - - 包括未来以Java后端开发为职业方向的在校学生、刚毕业入职的Java工程师以及未形成知识体系的Java工程师。通过阅读本书能够对Java工程师的必备技能有一个全局的认识,逐步形成自己的Java技术体系。 +> - 《Java并发编程实战》:此书是并发编程经典书籍,涵盖了并发编程的各种知识点以及相关理论知识。 -- 有经验的Java工程师 - - 有经验的Java工程师可以通过此书查漏补缺,巩固自己的开发技能,进一步加强自身的Java技术体系。 +> - 《七周七并发模式》:讲述了主流的七种并发编程模式。 -- 对Java后端开发感兴趣的非Java工程师 - - 非Java工程师可以通过此书了解Java工程师的技能体系,尤其对于其他语言的后端工程师来说,本书的很多内容也是通用的,并不局限于Java开发。 - -## 内容概览 - -- 第一章 后端技术导言 - - 本章主要从总体上描述后端技术的概念、组成、作用、需要的知识点,并给出了学习后端技术的建议。 - -- 第二章 Java项目工程化 - - 本章主要讲述Java项目工程化需要掌握的软件、技能等。 - -- 第三章 开发框架 - - 本章主要讲述Java后端开发中的一些主流框架的使用。 - -- 第四章 Spring - - 本章主要讲述Spring核心、数据操作以及一些常用组件的使用。 - -- 第五章 数据存储 - - 本章主要讲述Java应用中数据存储上使用的一些软件、服务等。 - -- 第六章 数据通信 - - 本章主要讲述Java应用中数据传输、通信上使用的一些软件、服务等。 - -- 第七章 Java编程进阶 - - 本章主要介绍一些Java开发中的高级特性以及在Java开发中非常流行的类库。 - -- 第八章 性能调优 - - 本章主要讲述如何对Java应用的性能进行分析和调优,并给出了开发建议。 - -- 第九章 安全技术 - - 本章主要对Java开发中常用的加密技术、HTTP以及防范各种攻击的方案做了阐述。 - -## 参考资料 - -在写作本书以及平时的工作中,笔者阅读、参考过很多书籍,以下是其中具有代表性的。对于本书讲述不够深入的地方,可以参考这些书籍进一步的学习。 - -- 《Effective Java(第2版)》:此书讲解了Java的一些高级特性和技巧,目前第三版的英文版已经在亚马逊上架。 - -- 《Java并发编程实战》:此书是并发编程经典书籍,涵盖了并发编程的各种知识点以及相关理论知识。 - -- 《七周七并发模式》:讲述了主流的七种并发编程模式。 - -- 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》:此书讲解了JVM的内存、GC、字节码、编译器等高级特性和优化实践。 +> - 《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》:此书讲解了JVM的内存、GC、字节码、编译器等高级特性和优化实践。 -- 《高性能MySQL(第3版)》:此书讲述了MySQL各种优化技巧,并结合原理给予讲解。 +> - 《高性能MySQL(第3版)》:此书讲述了MySQL各种优化技巧,并结合原理给予讲解。 -- 《Redis开发与运维》:此书在原理和实践层面对于Redis的使用优化做了详尽的描述。 +> - 《Redis开发与运维》:此书在原理和实践层面对于Redis的使用优化做了详尽的描述。 -- 《Redis设计与实现》:完整地讲解了Redis的内部运行机制,对Redis的大多数单机功能以及所有多机功能的实现原理进行了介绍,包括这些功能的核心数据结构以及关键的算法思想。旧版本(Redis 2.6)有免费电子版:http://origin.redisbook.com/ 。 +> - 《Redis设计与实现》:完整地讲解了Redis的内部运行机制,对Redis的大多数单机功能以及所有多机功能的实现原理进行了介绍,包括这些功能的核心数据结构以及关键的算法思想。旧版本(Redis 2.6)有免费电子版:http://origin.redisbook.com/。 -- 《深入分布式缓存》:此书涵盖了分布式原理、各种缓存软件/框架的使用以及相关技术在各大公司的典型实践。 +> - 《深入分布式缓存》:此书涵盖了分布式原理、各种缓存软件/框架的使用以及相关技术在各大公司的典型实践。 -- 《深入理解Elasticsearch(原书第2版)》:此书在原理层面讲述了对ES的使用和优化技巧。 +> - 《深入理解Elasticsearch(原书第2版)》:此书在原理层面讲述了对ES的使用和优化技巧。 -- 《Java性能权威指南》:此书是Java性能调优的权威书籍,几乎涵盖了Java调优的方方面面。 +> - 《Java性能权威指南》:此书是Java性能调优的权威书籍,几乎涵盖了Java调优的方方面面。 -- 《构建高性能Web站点》:此书从各种案例触发,讲解了高性能Web站点需要的各种优化技巧、实践经验等。 +> - 《构建高性能Web站点》:此书从各种案例出发,讲解了高性能Web站点需要的各种优化技巧、实践经验等。 -- 《白帽子讲Web安全》:本书基本涵盖了方方面面的Web安全技术,包括客户端安全、服务端安全等。 +> - 《白帽子讲Web安全》:本书基本涵盖了方方面面的Web安全技术,包括客户端安全、服务端安全等。 -- 《分布式系统:概念与设计(原书第5版)》:分布式系统理论的经典书籍,全面介绍了分布式系统的原理、体系结构、算法和设计。 +> - 《分布式系统:概念与设计(原书第5版)》:分布式系统理论的经典书籍,全面介绍了分布式系统的原理、体系结构、算法和设计。 -- 《Clean Architecture》: Uncle Bob的架构经典书籍,梳理了架构的定义、目的、架构设计原则、设计模式等,是架构入门的好书。 +> - 《Clean Architecture》: Uncle Bob的架构经典书籍,梳理了架构的定义、目的、架构设计原则、设计模式等,是架构入门的好书。 虽然以上书籍都是非常实用的参考资料,但就笔者自己来看,更为推崇的则是直接通过相关技术的官方文档来学习,既能够段炼自己的英文阅读能力,也能够直面相关技术的第一手文档,避免了在看相关书籍时被一些偶然的纰漏所误导。 -需要注意的是这里面的《Effective Java》(第三版英文版已经面世)和《Java并发编程实战》两本书都是基于Java的旧版本来写的,但是其讲述的很多东西并不过时,尤其是JDK底层源码、设计理论、优化思想等仍然适用于现在的Java开发。 - 此外,笔者的学习、工作笔记是平时工作中查阅网上资料并经过辨伪后记录下来的零散知识点,难免会有一些对网上零散资料的引用,特别对这些资料的原创者表示感谢,如果有侵权请联系我。 ## 勘误和支持 @@ -112,22 +48,19 @@ ![](images/mistakes.jpg) -## 致谢 - -由于工作以及个人身体等方面的原因,中间数次延期,历时一年多才完成本书。因此首先要特别感谢我的父母和妻子,在我写作此书的过程中给予了非常大的后勤支持和鼓励,让我能够专心地完成写作。 +## 联系方式 -同时要感谢中华万年历的同事们在平时的工作中给了我很多启发和思路,感谢公司的设计总监张喜亮抽出时间帮我修饰了一些图片;尤其要感谢CEO孙建在写作此书的过程中给予了我充分的信任和支持。 +邮箱:superhj1987@126.com -还要感谢我的前同事,也是我刚毕业时的工作导师尧飘海,在百忙之中审阅并给此书写序;也要感谢前同事张小川、阙杭宁和好友秦绪震、饶洵抽出宝贵时间校对了此书并给出了宝贵的建议。 +博客:https://rowkey.cn -最后,感谢电子工业出版社永恒的侠少找到我出版此书,并允许我一次次延期。也感谢付睿编辑的辛苦校对和修改,让本书得以顺利出版。 +微博:https://weibo.com/superhj1987 -也把此书献给我刚出生的女儿 - 依依。 +## 友情赞助 -## 联系方式 +友情赞助可扫码。 -邮箱:superhj1987@126.com + -博客:http://rowkey.me -微博:http://weibo.com/superhj1987 + \ No newline at end of file diff --git a/book/SUMMARY.md b/book/SUMMARY.md index 046c261..06814a8 100644 --- a/book/SUMMARY.md +++ b/book/SUMMARY.md @@ -17,7 +17,7 @@ - [3.2 对象关系映射](chapter3-framework/orm.md) - [3.3 日志](chapter3-framework/log.md) - [3.4 Web MVC](chapter3-framework/mvc.md) - - [3.5 响应式Web框架](chapter3-framework/reactive-web.md) + - [3.5 总结](chapter3-framework/end.md) * [第四章 Spring](chapter4-spring/README.md) - [4.1 Spring核心组件](chapter4-spring/spring.md) @@ -25,17 +25,19 @@ - [4.3 使用Spring Boot快速开发](chapter4-spring/spring-boot.md) - [4.4 Spring常用组件](chapter4-spring/spring-common.md) - [4.5 总结](chapter4-spring/end.md) - + * [第五章 数据存储](chapter5-datastore/README.md) - [5.1 关系型数据库-MySQL](chapter5-datastore/rds.md) - [5.2 非关系型数据库](chapter5-datastore/nosql.md) - [5.3 缓存](chapter5-datastore/cache.md) - [5.4 搜索引擎-Elasticsearch](chapter5-datastore/search.md) + - [5.5 总结](chapter5-datastore/end.md) * [第六章 数据通信](chapter6-datatrans/README.md) - [6.1 RESTful架构风格](chapter6-datatrans/rest.md) - [6.2 远程过程调用-RPC](chapter6-datatrans/rpc.md) - [6.3 消息中间件](chapter6-datatrans/message.md) + - [6.4 总结](chapter6-datatrans/end.md) * [第七章 Java编程进阶](chapter7-java/README.md) - [7.1 Java内存管理](chapter7-java/java-mm.md) @@ -48,7 +50,7 @@ * [第八章 性能调优](chapter8-profile/README.md) - [8.1 调优准备](chapter8-profile/ready.md) - [8.2 性能分析](chapter8-profile/analysis.md) - - [8.3 性能调优](chapter8-profile/chapter8-profile.md) + - [8.3 性能调优](chapter8-profile/profile.md) - [8.4 总结](chapter8-profile/end.md) * [第九章 安全技术](chapter9-security/README.md) @@ -68,4 +70,6 @@ * [附录F: 如何应对在线故障](appendix/online-debug.md) -* [附录G: 架构简明指南](appendix/arch-usage.md) \ No newline at end of file +* [附录G: 架构简明指南](appendix/arch-usage.md) + + diff --git a/book/appendix/arch-usage.md b/book/appendix/arch-usage.md index 443c10c..3b8fe26 100644 --- a/book/appendix/arch-usage.md +++ b/book/appendix/arch-usage.md @@ -6,7 +6,7 @@ 即:软件架构的目的就是解决软件复杂度(高性能、高可用、可扩展、低成本、安全、规模)带来的问题,将构建和维护系统需要的人力成本降到最低。 -因此,可以得出架构设计的关键思维就是判断和取舍(程序设计的关键思维是逻辑和实现),即如何选择技术、组合技术使得需要的人力资源最少。 +因此,可以得出架构设计的关键思维就是判断和取舍(程序设计的关键思维是逻辑和实现),即如何选择技术、组合技术使得需要的人力资源最少,达到降本增效的目的。 需要注意的一点是,脱离业务谈架构是不合理的,技术架构及其演进都是业务目标驱动的。 @@ -22,28 +22,10 @@ 架构的目的就是解决复杂度,主要包括高性能、高可用以及可扩展三方面。此外,分布式系统是架构工作中面对的典型复杂系统,对于其中常见的问题有一些常用应对手段。 -### 高性能 - -- 数据库集群 -- 缓存架构 -- 负载均衡 -- NoSQL - -**低延迟方案**[系统响应性能提升五板斧] - -- **异步**:队列缓冲、异步请求。 -- **并发**:利用多CPU多线程执行业务逻辑。 -- **就近原则**:缓存、梯度存储。 -- **减少IO**:合并细粒度接口为粗粒度接口、频繁的覆盖操作可以只做最后一次操作。这里一个需要特别注意的地方: **代码中尽量避免在循环中调用外部服务,更好的做法是使用粗粒度批量接口在循环外面只进行一次请求。** -- **分区**:频繁访问的数据集规模保持在合理的范围。 - -**高吞吐方案** - -- 分层调用 -- 异步并发 - ### 高可用 +> 高可用=系统构建在多机=分布式系统 + - 冗余:同城多活或者异地多活 - 降级:需要对各个关键节点建立降级预案。能够在超出预估流量时,保证大部分用户的服务是正常的。包括一个请求经过的多有节点。以轮训实现的直播系统为例: @@ -70,6 +52,29 @@ Kafka | 消息堆积 消息处理BG | 错误日志;JVM;消息处理数目,消息处理延时 基础资源 | 带宽、CPU利用率、内存、磁盘 +### 高性能 + +> 分布式系统的副产品 + +- 数据库集群 +- 缓存架构 +- 负载均衡 +- NoSQL:不局限于关系型数据库,在合适的场景下选择NoSQL数据库会带来性能的提升 +- 异构索引:分区情况下,为提升未按拆分键进行查询的场景的性能,通过构建异构索引表。先通过查询异构索引表得到目标记录的主键,然后再根据记录主键查询,从而避免全库全表扫描 + +**低延迟方案**[系统响应性能提升] + +- **异步**:队列缓冲、异步请求。 +- **并发**:利用多CPU多线程执行业务逻辑。 +- **就近原则**:缓存、梯度存储。 +- **减少IO**:合并细粒度接口为粗粒度接口、频繁的覆盖操作可以只做最后一次操作。这里一个需要特别注意的地方: **代码中尽量避免在循环中调用外部服务,更好的做法是使用粗粒度批量接口在循环外面只进行一次请求。** +- **分区**:频繁访问的数据集规模保持在合理的范围。 + +**高吞吐方案** + +- 分层调用:接入层、逻辑层、数据层,通过Proxy或者Router对逻辑层做集群管理 +- 异步并发 + ### 可扩展 - 分层架构/简洁架构:单向依赖,职责清晰。 @@ -87,9 +92,7 @@ Kafka | 消息堆积 1. 海量请求问题 - - 高吞吐:分层调用、异步并发 - - 低延迟:系统响应性能提升五板斧(见上文)、NoSQL - - 高可用:冗余、降级 + 本质即如何达到高吞吐、低延迟、高可用,上文已经讲述。 1. 大量服务器管理 @@ -237,9 +240,11 @@ ps: 在容量预估中,机器数目的计算遵循DID原则:20倍设计、3 1. 上线双写:在业务系统里写代码,同时向新旧数据存储写入数据。此步骤完成后,需要进行一致性验证,包括存储维度和业务维度。前者指对比原数据存储和新数据存储中的数据对比,后者指从用户看到的数据维度进行对比。 2. 历史数据迁移:将历史数据从旧存储迁移到新存储。包括离线和在线两种。离线是编写批量处理程序或者依靠数据存储的同步机制从旧存储查询历史数据(开启双写以前的数据)插入到新存储中。在线指的是依赖数据存储的同步机制在线同步数据,如MySQL的binlog、MongoDB的OpLog。此过程,建议在部分数据迁移后就进行一致性验证,通过后再全量数据迁移。 -3. 切读:通过灰度的方式逐渐切换请求到新系统上,灰度可以通过在代码中埋入开关来逐步的放大读新系统的请求量。一般的流程:预发布环境(验证代码运行正常)->办公室环境/线上环境白名单(内部用户,验证功能正常)->线上环境百分比0.1%、1%、%10%(进一步验证功能正常以及性能和资源压力)->线上环境全量。此过程建议持续一到两周。 +3. 切读:通过灰度的方式逐渐切换请求到新系统上,灰度可以通过在代码中埋入开关来逐步的放大读新系统的请求量。一般的流程:预发布/Tcpcopy环境(验证代码运行正常)->办公室环境/线上环境uid白名单(内部用户,验证功能正常)->线上环境百分比0.1%、1%、%10%(进一步验证功能正常以及性能和资源压力)->线上环境全量。此过程建议持续一到两周。 4. 清理:数据迁移验证通过后,清理业务系统的双写代码和开关代码等逻辑代码、旧存储的数据和配套系统以及旧的资源等。 +某些情况下,可以先做历史数据搬迁,然后再写入新数据。需要谨慎的处理搬迁这段时间里产生的新数据,一般使用 queue 缓存写入的方式,称为“追数据”。 + 此外,如果是单一功能的在线数据迁移,可以参考Redis Cluster数据重分配的实现机制。 1. 离线程序迁移数据,并维护数据迁移状态:未迁移、迁移中、迁移完成。 diff --git a/book/appendix/build-cmd.md b/book/appendix/build-cmd.md index a68fc24..8c08a71 100644 --- a/book/appendix/build-cmd.md +++ b/book/appendix/build-cmd.md @@ -24,7 +24,7 @@ Maven版本:3.3.9 - 部署非Maven项目的jar包 - `mvn deploy:deploy-file -DgroupId=[groupId] -DartifactId=[artifactId] -Dversion=[version] -Dpackaging=jar -Dfile=[jarFilePath] -Durl=[repositoryUrl]` + `mvn deploy:deploy-file -DgroupId=[groupId] -DartifactId=[artifactId] -Dversion=[version] -Dpackaging=jar -Dfile=[jarFilePath] -Durl=[repositoryUrl] -DrepositoryId=[repositoryId]` - 安装非Maven项目的jar包到本地 diff --git a/book/appendix/git-usage.md b/book/appendix/git-usage.md index e77bc20..6dc8973 100644 --- a/book/appendix/git-usage.md +++ b/book/appendix/git-usage.md @@ -87,10 +87,17 @@ Git的配置,分为三个级别: ssh -T git@github.com #测试是否成功 #使用ssh-agent管理密码,避免后续需要身份验证的地方需要输入密码 - ssh-add -K private_key_path #添添加私钥到ssh-agent中,使用-K参数将密钥加入到密钥链中 + ssh-add -K private_key_path #添添加私钥到ssh-agent中,使用-K参数将密钥加入到密钥链中(Mac OS特有参数) ssh-add -l #查看当前计算机中存储的密钥 ssh-add -d public_key_path #将对应的私钥从ssh-agent删除 ``` + +1. 使用http/https协议访问Git仓库时缓存密码 + + ``` + # 默认不缓存;cache缓存在内存中,默认15分钟失效;store,明文存储在磁盘上,永不过期;osxkeychain,Mac OS特有,加密存储在用户钥匙串中,永不过期;winstore和manager是Windows下特有,取决于安装的是git-credential-winstore/git-credential-manager(GitGUI自带) + git config --global credential.helper [cache|store|osxkeychain|winstore/manager] + ``` ## 取得项目的Git仓库 @@ -179,7 +186,7 @@ Git的配置,分为三个级别: ``` git commit [file1] [file2] #提交会提示输入本次提交说明 - git commit -m [messag] #直接附带提交说明 + git commit -m [message] #直接附带提交说明,message用双引号包裹是单行信息,用单引号则可以提交多行信息 git commit --amend #修改最后一次提交 git commit -v #提交时显示所有diff信息 git commit --amend -m [message] #使用一次新的commit,替代上一次提交,如果代码没有任何新变化,则用来改写上一次commit的提交信息 @@ -301,6 +308,7 @@ git branch #列出本地分支 git branch -r #列出远端分支 git branch -a #列出所有本地分支和远程分支 git branch -v #查看各个分支最后一个提交对象的信息 +git branch -vv #使用两个v,额外显示本地分支和远程分支的追踪关系 git branch --merge #查看已经合并到当前分支的分支 git branch --no-merge #查看为合并到当前分支的分支 diff --git a/book/appendix/java-profile.md b/book/appendix/java-profile.md index c0b8795..d594e29 100644 --- a/book/appendix/java-profile.md +++ b/book/appendix/java-profile.md @@ -165,7 +165,7 @@ #-XX:+PrintCompilation #输出JIT编译情况,慎用 -XX:+TieredCompilation #启用多层编译,JDK8默认开启 -XX:CICompilerCount=4 #编译器数目增加 --XX:-UseBiasedLocking #取消偏向锁。偏向锁会触发进入Safepoint,引起停顿,因此高并发应用建议取消偏向锁。 +-XX:-UseBiasedLocking #取消偏向锁。偏向锁会触发进入Safepoint,引起停顿,因此高并发应用建议取消偏向锁 -XX:AutoBoxCacheMax=20000 #自动装箱的缓存数量,如int默认缓存为-128~127 -Djava.security.egd=file:/dev/./urandom #替代默认的/dev/random阻塞生成因子 -XX:+AlwaysPreTouch #启动时访问并置零内存页面,大堆时效果比较好 @@ -196,6 +196,8 @@ -XX:GCTimeLimit=98 #GC占用时间超过多少抛出OutOfMemoryError -XX:GCHeapFreeLimit=2 #GC回收后小于百分之多少抛出OutO fMemoryError -Xloggc:/home/logs/gc.log #GC日志路径,重启后会被清空 +-XX:+PrintCommandLineFlags #将每次JVM启动的参数输出到stdout,以供追溯。 +-XX:-OmitStackTraceInFastThrow #对一些特定的异常类型(NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException、ArrayStoreException、ClassCastException)的Fast Throw优化,如果检测到在代码里某个位置连续多次抛出同一类型异常的话,会用Fast Throw方式来抛出异常,不带上异常栈信息。在连续抛出大量重复异常并且很难回溯前面完整栈信息时可以关闭此选项使得不会进行Fast Throw优化 #-XX:+UseGCLogFileRotation #开启GC日志滚动输出 #-XX:NumberOfGCLogFiles=100 #轮转日志数目最大为100,超过则覆盖 #-XX:GCLogFileSize=100M #GC轮转日志最大尺寸100mb,超过则另起一个日志文件 diff --git a/book/appendix/media/apm.png b/book/appendix/media/apm.png new file mode 100644 index 0000000..5283c89 Binary files /dev/null and b/book/appendix/media/apm.png differ diff --git a/book/appendix/media/distribute-qa.jpg b/book/appendix/media/distribute-qa.jpg new file mode 100644 index 0000000..b515d1f Binary files /dev/null and b/book/appendix/media/distribute-qa.jpg differ diff --git a/book/appendix/media/low-level.png b/book/appendix/media/low-level.png new file mode 100644 index 0000000..f2c3568 Binary files /dev/null and b/book/appendix/media/low-level.png differ diff --git a/book/appendix/media/sixthink.jpg b/book/appendix/media/sixthink.jpg new file mode 100644 index 0000000..9f59159 Binary files /dev/null and b/book/appendix/media/sixthink.jpg differ diff --git a/book/appendix/mongo-usage.md b/book/appendix/mongo-usage.md new file mode 100644 index 0000000..ab522cc --- /dev/null +++ b/book/appendix/mongo-usage.md @@ -0,0 +1,166 @@ +# 附录D: MongoDB常用命令 + +MongoDB版本:3.2.7 + +## 1. 基本操作 + +* db.getMongo():取得当前服务器的连接对象。 +* db.createUser(user, writeConcern) :添加用户。 +* db.dropUser(username):删除用户。 +* db.system.users.find():查看系统所有用户列表。 +* db.system.users.remove({user:"mongouser"}): 删除用户。 +* db.getUsers():查看当前数据库的用户。 +* db.auth(usrename,password);验证用户。 +* db.getName():返回当操作数据库的名称。 +* db.createCollection(name):创建一个数据集。 +* db.currentOp():查看数据库的当前操作。 +* db.dropDataBase():删除当前数据库。 +* db.getCollection(collectonName):取得一个数据集合。 +* db.getCollenctionNames():取得所有数据集合的名称列表。 +* db.getLastError():返回最后一个错误的提示消息。 +* db.getLastErrorObj():返回最后一个错误的对象。 +* db.getReplicationInfo():获得复制集的信息。 +* db.printReplicationInfo():打印复制集的信息。 +* db.printCollectionStats():返回当前库的数据集合状态。 +* db.printSlaveReplicationInfo():打印从数据库的复制集信息。 +* db.printShardingStatus():打印分片状态。 +* db.commandHelp(command):显示命令的帮助信息。 +* db.runCommand(cmdObj):运行一个数据库命令。 +* db.setProfilingLevel(level,slowms):设置数据库的优化级别(0=off,1=slow,2=all)以及慢查询的耗时阈值。 +* db.getProfilingStatus():获取数据库的优化级别和慢查询的耗时阈值。 +* db.system.profile.find():查看收集到的慢查询。 +* db.version():返回当前程序的版本信息。 +* db.serverStatus().connections:连接数信息,其中current数值+available数值就是当前mongodb最大连接数。 +* db.serverStatus().mem:内存占用信息。 +* db.cloneDataBase(fromhost):从目标服务器克隆一个数据库。 +* db.copyDatabase(fromdb,todb,fromhost):复制数据库:fromdb-源数据库名称,todb-目标数据库名称,fromhost-源数据库服务器地址。 +* db.repairDatabase():修复当前数据库。 +* db.killOp():停止/杀死在当前库的当前操作。 +* db.shutdownServer():安全关闭当前服务程序。 + +## 2. 数据集操作 + +* db.test_collection.drop():删除数据集。 +* db.createCollection(“test_collection”):创建集合。 +* db.test_collection.renameCollection("test_collection1"):重命名集合。 +* db.test_collection.find({status:1}):返回test_collection数据集status=1的数据集。 +* db.test_collection.find({status:1}}).count():返回test_collection数据集中status=1的数据总数。 +* db.test_collection.find({status:1}).limit(3):返回test_collection数据集中status=1的前三条数据。 +* db.test_collection.find({status:1}).skip(2):返回test_collection数据集中status=1的从第三条开始的数据。 +* db.test_collection.find({status:1}).limit(24).skip(8):返回test_collection数据集中status=1的从第九条开始的24条数据。 +* db.test_collection.find({status:1}}).sort():返回test_collection数据集中status=1的有序数据。 +* db.test_collection.findOne([query]) 返回符合条件的一条数据。 +* db.test_collection.getIndexes():返回此数据集的索引信息。 +* db.test_collection.mapReduce(mayFunction,reduceFunction):执行MapReduce操作。 +* db.test_collection.remove(query):在数据集中删除一条数据。 +* db.test_collection.remove({}):清空数据集合。 +* db.test_collection.save(obj):往数据集中插入/更新一条数据。 +* db.test_collection.stats():返回此数据集的状态。 +* db.test_collection.storageSize():返回此数据集的存储大小 +* db.test_collection.totalIndexSize():返回此数据集的索引文件大小。 +* db.test_collection.totalSize():返回此数据集的总大小。 +* db.test_collection.update(query,object[,upsert_bool]):在此数据集中更新一条数据。 +* db.test_collection.createIndex(keys[,options]):创建索引。 +* db.test_collection.getIndexes():查看索引。 +* db.test_collection.dropIndex('[indexName]'):删除索引。 + +## 3. MongoDB语法与关系型数据库SQL语法比较 + +* db.test_collection.find({'name':'testname'}) <-> select * from test_collection where name='testname' +* db.test_collection.find({$or:[{'name':'testname'},{'name':'testname2'}]}) <-> select * from test_collection where name='testname' or testname='testname2' +* db.test_collection.find() <-> select * from test_collection +* db.test_collection.find({'status':1}).count() <-> select count(*) from test_collection where status=1 +* db.test_collection.find().skip(10).limit(20) <-> select * from test_collection limit 10,20 +* db.test_collection.find({'status':{$in:[1,2]}}) <-> select * from test_collection where status in (1,2) +* db.test_collection.find().sort({'status':-1}) <-> select * from test_collection order by status desc +* db.test_collection.distinct('name',{'age':{$lt:25}}) <-> select distinct(name) from test_collection where age < 1 +* db.test_collection.group({key:{'name':true},cond:{'name':'foo'},reduce:function(obj,prev){prev.msum+=obj.star;},initial:{msum:0}}) <-> select name,sum(stat) from test_collection group by name +* db.test_collection.find('this.age<25',{name:1}) <-> select name from test_collection where age < 20 +* db.test_collection.insert({'name':'testname','age':25})<->insert into test_collection ('name','age') values('testname',25) +* db.test_collection.remove({}) <-> delete from test_collection +* db.test_collection.remove({'age':25}) <-> delete from test_collection where age=25 +* db.test_collection.remove({'age':{$lt:20}}) <-> delete from test_collection where age<25 +* db.test_collection.remove({'age':{$lte:20}}) <-> delete from test_collection where age<=25 +* db.test_collection.remove({'age':{$gt:20}}) <-> delete from test_collection where age>25 +* db.test_collection.remove({'age':{$gte:20}}) <-> delete from test_collection where age>=2 +* db.test_collection.remove({'age':{$ne:20}}) <-> delete from test_collection where age!=25 +* db.test_collection.updateMany({'name':'testname'},{$set:{'age':30}}) <-> update test_collection set age=30 where name='testname' +* db.test_collection.updateMany({'name':'testname'},{$inc:{'age':2}}) <-> update test_collection set age=age+2 where name='testname' +* db.test_collection.find({name: /testname/}) <-> select * from test_collection where name like ‘%testname%’; +* db.test_collection.find({name: /^testname/}) <-> select * from test_collection where name like ‘testname%’; + +## 4. 开启安全认证 + +### 创建管理员用户 + +``` +use admin +db.createUser( + { + user: "root", + pwd: "root123", + roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] #roles设置为root则为超级用户权限 + } +) + +mongod --auth --port 27017 --dbpath /data/db --authenticationDatabase "admin" +use admin; +db.auth("root","root123"); +``` + +### 创建用户 +``` +use test_db; +db.createUser( + { + user: "mongouser", + pwd: "mongo123", + roles: [ + { role: "readWrite", db: "test_db" } + ] + } +) +``` + +### 修改密码 + +``` +db.changeUserPassword("mongouser", "123456”) +``` + +### 获取某用户的权限信息 + +``` +db.getUser("mongouser") +``` + +### 获取某角色的权限信息 + +``` +db.getRole( "read", { showPrivileges: true } ) +``` + +### 赋予权限 + +``` +use test_db; +db.grantRolesToUser( + "mongouser", + [ + { role: "read", db: “test_db" } + ] +) +``` + +### 删除权限 + +``` +use test_db; +db.revokeRolesFromUser( + "mongouser", + [ + { role: "readWrite", db: "test_db" } + ] +) +``` + diff --git a/book/appendix/mysql-usage.md b/book/appendix/mysql-usage.md new file mode 100644 index 0000000..a274a78 --- /dev/null +++ b/book/appendix/mysql-usage.md @@ -0,0 +1,454 @@ +# 附录C: MySQL常用命令 + +MySQL版本:5.5.19 + +## 系统命令 + +1. 启动MySQL + + ``` + mysqladmin start + /ect/init.d/mysql start + ``` + +2. 重启MySQL + + ``` + mysqladmin restart + /ect/init.d/mysql restart + ``` + +3. 关闭MySQL + + ``` + mysqladmin shutdown + /ect/init.d/mysql shutdown + ``` + +4. 连接本机上的MySQL + + 进入目录`mysql\bin`,键入命令`mysql -uroot -p`,回车后提示输入密码。使用`exit`退出MySQL。 + +5. 修改MySQL密码 + + ``` + mysqladmin -u用户名 -p旧密码 password 新密码 + ``` + + 或进入MySQL命令行后设置 + + ``` + set password for root=password("root"); + ``` + +6. 增加新用户 + + ``` + grant select on 数据库.* to 用户名@登录主机 identified by "密码"; + ``` + + 示例:增加一个用户test密码为123,让他可以在任何主机上登录,并对所有数据库有查询、插入、修改、删除的权限。以root用户连入MySQL,然后键入以下命令: + + ``` + grant select,insert,update,delete on *.* to test@"% " identified by "123"; + ``` + +7. 刷新MySQL的系统权限相关表 + + ``` + flush privileges + ``` + + 新设置用户或更改密码后需刷新MySQL的系统权限相关表 + +1. 查看MySQL支持的存储引擎 + + ``` + show engines + ``` + +1. 查看MySQL当前的默认存储引擎 + + ``` + show variables like '%storage_engine%'; + ``` + +1. 查看Mysql服务器上的版本 + + ``` + select version(); + ``` + +1. 查看数据库连接情况 + + ``` + show variables like '%max_connections%’; #查看最大连接数设置 + set global max_connections = 200; #设置最大连接数 + + select * from information_schema.processlist where db=''; #查看进程/连接列表,可指定数据库。和show processlist一样 + ``` + +1. 查看死锁信息 + + ``` + show engine innodb status; #LATEST DETECTED DEADLOCK这一栏即死锁信息 + ``` + +1. 开启慢查询日志 + + ``` + show variables like '%slow%’; //查看慢查询日志配置 + set global slow_query_log='ON'; //开启慢查询 + set global long_query_time=4;//这只慢查询语句的耗时阈值 + ``` + +1. 查询结果输出到文件 + + ``` + [select query] into outfile '[filePath]'; + + pager cat > [filePath]; #所有查询结果都自动写入指定文件中,并前后覆盖 + + mysql -h [host] -u [user] -p [password] -P [port] -e "[query]" > [filePath] + ``` + +## 数据操作 + +首先登录到MySQL中,有关操作都是在MySQL的提示符下进行,而且每个命令以分号结束。 + +1. 显示数据库列表 + + ``` + show databases; + ``` + +2. 显示库中的数据表 + + ``` + show tables; + ``` + +3. 显示数据表的结构 + + ``` + describe 表名; + ``` + +4. 建库 + + ``` + create database 库名; + + create database db_name default character set utf8 collate utf8_general_ci; + ``` + + create database 的语法: + + ``` + create {database | schema} [if not exists] db_name [create_specification [, create_specification ] ...] create_specification : [default] character set charset_name | [default] collate collation_name + ``` + +5. 建表 + + ``` + create table 表名(字段设定列表); + ``` + +6. 删库和删表 + + ``` + drop database 库名; + drop table 表名; + ``` + +7. 将表中记录清空 + + ``` + delete from 表名; + truncate table 表名; #不同于delete, 不用扫描全表 + ``` + +8. 显示表中的记录: + + ``` + select * from 表名; + ``` + +1. 添加列 + + ``` + alter table 数据表名 add 新列名 新列类型 default 0 comment; + ``` + +1. 修改列名 + + ``` + alter table 数据表名 change 原列名 新列名 新列类型; + ``` + +1. 修改列 + + ``` + alter table 表名 modify column 列名 类型; + ``` + +1. 删除列 + + ``` + alter table 表名 drop column 列名; + ``` + +1. 修改表名 + + ``` + rename table 旧表名 to 新表名; + ``` + +1. 添加索引 + + ``` + alter table table_name add index index_name (column_list); + alter table table_name add unique index_name (column_list); + alter table table_name add primary key (column_list); + ``` + +1. 删除索引 + + ``` + drop index index_name on talbe_name + alter table table_name drop index index_name + alter table table_name drop primary key + ``` + +1. 查看索引 + + ``` + show index from tblname; + show keys from tblname; + ``` + +1. 复制表结构及数据到新表 + + ``` + create table 新表 select * from 旧表; + ``` + +1. 只复制表结构到新表 + + ``` + create table 新表 select * from 旧表 where 1=2; + create table 新表 like 旧表; + ``` + +1. 复制旧表的数据到新表(假设两个表结构一样) + + ``` + insert into 新表 select * from 旧表; + ``` + +1. 复制旧表的数据到新表(假设两个表结构不一样) + + ``` + insert into 新表(字段1,字段2,.......) select 字段1,字段2,...... from 旧表 + ``` + +1. 设置表的自增主键起始值 + + ``` + alter table table_name AUTO_INCREMENT = 10000; + ``` + +## 数据的导入导出 + +1. 文本数据转到数据库中 + + 文本数据应符合的格式:字段数据之间用tab键隔开,null值用空格字符来代替。例如: + + ``` + 1 name test 2017-1-1 + ``` + + 数据传入命令 + + ``` + load data local infile "文件名" into table 表名; + ``` + +2. 导出数据库和表 + + 将数据库news中的所有表备份到news.sql文件,news.sql是一个文本文件,文件名任取。 + + ``` + mysqldump --opt news > news.sql + ``` + + 将数据库news中的author表和article表备份到author.article.sql文件,author.article.sql是一个文本文件,文件名任取。 + + ``` + mysqldump --opt news author article > author.article.sql + ``` + + 将数据库dbl和db2备份到news.sql文件,news.sql是一个文本文件,文件名任取。 + + ``` + mysqldump --databases db1 db2 > news.sql + ``` + + 把host上的以用户user、密码pass的数据库dbname导入到文件file.dump中 + + ``` + mysqldump -h host -u user -p pass --databases dbname > file.dump + ``` + + 将所有数据库备份到all-databases.sql文件,all-databases.sql是一个文本文件,文件名任取。 + + ``` + mysqldump --all-databases > all-databases.sql + ``` + +3. 导入数据 + + 导入数据库: + + ``` + mysql < all-databases.sql + ``` + + 在mysql命令行导入表: + + ``` + source news.sql; + ``` + +## 编码操作 + +- 查看数据库编码 + + ``` + show create database db_name; + ``` + +- 查看数据表编码,包括表使用的数据库引擎 + + ``` + show create table tbl_name; + ``` + +- 查看字段编码 + + ``` + show full columns from tbl_name; + ``` + +- 改变整个MySQL的编码, 启动MySQL的时候,mysqld_safe命令行加入 + + ``` + --default-character-set=gbk; + ``` + +- 改变某个库的编码,在MySQL提示符后输入命令 + + ``` + alter database db_name default character set gbk; + ``` + +- 把表默认的字符集和所有字符列(char,varchar,text)改为新的字符集 + + ``` + alter table tbl_name convert to character set character_name [collate ...]; + ``` + + 示例如下: + + ``` + alter table logtest convert to character set utf8 collate utf8_general_ci; + + alter table table_name convert to character set utf8mb4 collate utf8mb4_bin; #使得数据库支持emoji + ``` + +- 修改表的默认字符集 + + ``` + alter table tbl_name default character set character_name [collate...]; + ``` + +- 修改字段的字符集 + + ``` + alter table tbl_name change c_name c_name character set character_name [collate ...]; + ``` + +## 数据库元信息查询 + +information_schema数据库中保存了各个数据库以及表的元信息,主要包括: + +- schemata表:提供了当前MySQL实例中所有数据库的信息。是`show databases`的结果来源。 +- tables表:提供了关于数据库中的表的信息,包括视图。详细表述了某个表属于哪个schema、表的类型、表使用的引擎以及创建时间等信息。是`show tables from [schemaName]`和`show table status from [schemaName] like '[tableName]'`的结果来源。 +- columns表:提供了表中的列信息。详细表述了某张表的所有列以及每个列的信息。是`show columns from [tableName]`的结果来源。 +- statistics表:提供了关于表索引的信息。是`show index from [tableName]`的结果来源。 +- user_privileges表:给出了关于用户权限的信息。该信息源自mysql.user授权表。 +- schema_privileges表:给出了关于方案(数据库)权限的信息。该信息来自mysql.db授权表。 +- table_privileges表:给出了关于表权限的信息。该信息源自mysql.tables_priv授权表。 +- column_privileges表:给出了关于列权限的信息。该信息源自mysql.columns_priv授权表。 +- table_constraints表:描述了存在约束的表,以及表的约束类型。 + +例如,可通过tables查询某个数据表的创建时间: + +``` +select create_time from tables where table_schema='数据库名' and table_name='表名'; +``` +## 数据库性能信息查询 + +performance_schema数据库用于收集数据库服务器性能参数,其中所有表的存储引擎为performance_schema。此功能MySQL5.5是默认关闭的,从5.6版本后变为默认开启。此功能开关如下: + +``` +[mysqld] +performance_schema=ON/OFF +``` + +其中的常用数据表如下: + +- setup_actors:配置用户纬度的监控,默认监控所有用户。 +- setup_consumers:配置events的消费者类型,即收集的events写入到哪些统计表中。 +- setup_objects:配置监控对象,默认对mysql,performance_schema和information_schema中的表都不监控,而其它DB的所有表都监控。 +- setup_timers:配置每种类型指令的统计时间单位。MICROSECOND表示统计单位是微妙,CYCLE表示统计单位是时钟周期,时间度量与CPU的主频有关,NANOSECOND表示统计单位是纳秒。但无论采用哪种度量单位,最终统计表中统计的时间都会转换到皮秒。(1秒=1000000000000皮秒) +- file_instances:文件实例, 记录了系统中打开了文件的对象,包括ibdata文件,redo文件,binlog文件,用户的表文件等。 +- rwlock_instances: 读写锁同步对象实例,记录了系统中使用读写锁对象的所有记录,其中name为 wait/synch/rwlock/*。 +- socket_instances:活跃会话对象实例,记录了thread_id,socket_id,ip和port,其它表可以通过thread_id与socket_instance进行关联,获取IP-PORT信息,能够与应用对接起来。 +- - socket_summary_by_instance、socket_summary_by_event_name:socket聚合统计表。 +- events_waits_current:记录了当前线程等待的事件。 +- events_waits_history:记录了每个线程最近等待的10个事件。 +- events_waits_history_long:记录了最近所有线程产生的10000个事件。 +- events_stages_current:记录了当前线程所处的执行阶段。同events_waits_current一样,events_stages_history、events_stages_history_long是历史记录。 +- events_statements_current:通过 thread_id+event_id可以唯一确定一条记录。Statments表只记录最顶层的请求,SQL语句或是COMMAND,每条语句一行。event_name形式为statement/sql/*,或statement/com/*。同events_waits_current一样,events_statements_history、events_statements_history_long是历史记录。 +- events_waits_summary_by_thread_by_event_name:按每个线程和事件来统计,thread_id+event_name唯一确定一条记录。包括总的等待时间、最小等待时间、平均等待时间以及最大等待时间。 +- table_lock_waits_summary_by_table:聚合了表锁等待事件,包括internal lock 和 external lock。 +- table_io_waits_summary_by_table:根据wait/io/table/sql/handler,聚合每个表的I/O操作(逻辑IO纬度)。 +- users:记录用户连接数信息。 +- hosts:记录了主机连接数信息。 +- accounts:记录了用户主机连接数信息。 +- threads:监视服务端的当前运行的线程。 + +几个常用应用示例如下: + +- 统计哪个SQL执行最多 + + ``` + SELECT SCHEMA_NAME,DIGEST_TEXT,COUNT_STAR,SUM_ROWS_SENT,SUM_ROWS_EXAMINED,FIRST_SEEN,LAST_SEEN FROM events_statements_summary_by_digest ORDER BY COUNT_STAR desc LIMIT 1\G + ``` + +- 哪个SQL平均响应时间最长 + + ``` + SELECT SCHEMA_NAME,DIGEST_TEXT,COUNT_STAR,AVG_TIMER_WAIT,SUM_ROWS_SENT,SUM_ROWS_EXAMINED,FIRST_SEEN,LAST_SEEN FROM events_statements_summary_by_digest ORDER BY AVG_TIMER_WAIT desc LIMIT 1\G + ``` + +- 哪个索引没有使用过 + + ``` + SELECT OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME FROM table_io_waits_summary_by_index_usage WHERE INDEX_NAME IS NOT NULL AND COUNT_STAR = 0 AND OBJECT_SCHEMA <> 'mysql' ORDER BY OBJECT_SCHEMA,OBJECT_NAME; + ``` + +需要注意的是,MySQL5.5开启此功能即使没有数据可收集,也会有性能损失,慎重开启。MySQL5.6后做了改善,直到收集信息才会激活。 + + diff --git a/book/appendix/online-debug.md b/book/appendix/online-debug.md index 777c887..41e2603 100644 --- a/book/appendix/online-debug.md +++ b/book/appendix/online-debug.md @@ -4,7 +4,7 @@ ## 应对思路 -第一时间上报给自己的直属领导或者相关负责人,由其把控后续流程,并在可能的情况下及时周知问题、影响范围、解决方案、预计恢复时间等。 +第一时间上报给自己的直属领导或者相关负责人,由其把控后续流程,并在可能的情况下及时周知所有可能受影响方:问题、影响范围、解决方案、预计恢复时间等。 1. 根据经验来分析。如果应急团队中有人对相应的问题有经验,并能确定能够通过某种手段恢复系统的正常运行,那么应该第一时间恢复(回滚等),同时务必要保留现场,以备后续对问题的定位和修复;如果没有人有经验,则需要使用比较粗暴的办法保证服务可用,如定时重启、限流、降级等。 2. 业务负责人、技术负责人、核心研发人员、架构师、运维工程师以及运营人员对问题的原因进行快速分析。分析的过程需要首先考虑系统近期的变化,包括以下几方面: diff --git a/book/chapter1-servertech/README.md b/book/chapter1-servertech/README.md index a365089..f1644f6 100644 --- a/book/chapter1-servertech/README.md +++ b/book/chapter1-servertech/README.md @@ -13,7 +13,7 @@ - 稳定性:也叫做鲁棒性、健壮性,即服务在异常和危险情况下保持稳定的能力。 - 容错性:在服务出现错误或者异常的时候,能够继续提供一定服务的能力,主要强调的是容许误差、故障的能力。显然,此指标会影响可用率,容错性越好,那么系统的可用率也就会越高。 - 扩展性:主要指的服务的动态扩展能力,即通过扩展(而非修改)现有系统的能力来满足需求的能力。 -- 维护性:指的是修正服务错误、修改服务功能的能力。 +- 维护性:指的是监控/修正服务错误、修改服务功能的能力,主要和运维监控方面的工作相关。 - 安全性:保障系统以及用户数据安全性的能力,包括保障系统不被非法入侵、用户数据不被泄漏等。 除此之外很多书籍和资料还会有可靠性、可用性等说法,其实本质上是以上指标的另一种说法而已。一般来说后端服务的设计目标主要包括高可用、低延迟、高吞吐、高并发、可容错、可扩展、可维护、稳定、安全。后端技术也基本是围绕着这几个目标来进行的。 diff --git a/book/chapter1-servertech/how-to-study.md b/book/chapter1-servertech/how-to-study.md index e8478c3..edfc372 100644 --- a/book/chapter1-servertech/how-to-study.md +++ b/book/chapter1-servertech/how-to-study.md @@ -10,6 +10,12 @@ - 自我总结 - 学会规划 +开篇之前,这里先给出一个高效学习方法框架(摘录自[《技术人如何更好地把控发展趋势?》](https://mp.weixin.qq.com/s/Cedl9lIk2mAd9b_NUCnj_g)),如下图所示。 + +![](media/study.png) + +本文所讲的内容与此方法论不谋而合。 + ## 1.3.1 扎实的计算机基础知识 - 数据结构和算法:程序是由数据和算法组成的,因此这两个东西是计算机软件的基础。诸如B树、哈希表、栈以及七大排序算法、查找算法这些,在很多软件的代码中都可以看得到。有时候,一个优秀的工程师和一个普通工程师的区别也就在于是否能够合理使用合适的数据结构和算法。 @@ -58,7 +64,7 @@ 这一点主要说的是要学会用计算机思维来思索问题。所谓的计算机思维本质就是冯诺依曼体系所描述:程序存储,顺序执行。 -经常听到的程序员买苹果的段子就是按照for循环、if else等逻辑来判断苹果好坏、计数,并做防御性检查等,这就是计算机思维的体现。而之前一个很火的电影《天才枪手》中记忆选择题答案如果使用计算机思维,那么用两位bit可以表示四个答案,再将四位bit转换为一个十六进制数字,记忆答案就可以减少一般的存储量。 +经常听到的程序员买苹果的段子就是按照for循环、if else等逻辑来判断苹果好坏、计数,并做防御性检查等,这就是计算机思维的体现。而之前一个很火的电影《天才枪手》中记忆选择题答案如果使用计算机思维,那么用两位bit可以表示四个答案,再将四位bit转换为一个十六进制数字,记忆答案就可以减少一半的存储量。 如此,诸如二进制存储、防御编程、循环遍历、位运算、多进程/线程以及常用的数据结构和算法等都应该成为不自觉的意识。能够在遇到问题时,下意识地用这些东西来思考,将人类语言的需求转化为计算机语言。 @@ -122,9 +128,18 @@ method1 | | | | | ... ![](media/programmer-pyramid.png) - 初/中级程序员:这一层级的程序员包括入门没有多久的新手以及相关技能不熟练的程序员。已经掌握了基本的程序编写技能,但无法保证代码质量,需要在高级以上程序员的指导才能达到工作目标。从职业发展来看,这一层级应该是一个过渡层级,长期处于此层的程序员处在非常不利的状态。 -- 高级程序员:渡过了第一层级之后,除了掌握了基本的编码技能,还熟练掌握了设计模式、常用类库等工作常用技能,并能够熟练的将需求实现成程序,保质保量的完成交付。此层级的程序员才真正成为工程师,是工作的中坚力量,除了能够很好地完成自己的工作外,也能够指导初/中级程序员完成工作。 -- 项目经理、架构师:高级程序员再往前发展,会有两个方向。走项目管理会成为项目经理,主要负责项目进度把控、协调沟通等,比较偏向于管理;走技术路线,会发展为资深程序员、架构师,能够把控一个系统的整体架构或者精通某一领域技术,做好技术选型和系统设计工作。 -- 部门经理/技术总监、技术专家:这一层级的部门经理和技术总监已经基本脱离了程序员的含义,都不再是单纯的技术工作,管理占了日常工作的大部分。相比起之前,更需要的是对团队整体的把控,包括做事、带人、看方向等。而深钻某一领域技术的程序员在这一层级会成为技术专家,为技术团队提供领域技术的指导咨询工作。 -- CTO: 这一层级是程序员最顶级的职位。其需要的能力包括资源整合规划能力、技术战略规划能力、领导艺术、企业文化和制度建设能力。其中资源整合包括技术资源整合、知识整合、自我行销、人际关系整合。 +- 高级程序员:渡过了第一层级之后,除了掌握了基本的编码技能,还熟练掌握了设计模式、常用类库等工作常用技能,并能够熟练的将需求实现成程序,保质保量的完成交付。此层级的程序员才真正成为工程师,是工作的中坚力量,除了能够很好地完成自己的工作外,也能够指导初/中级程序员完成工作。能力图谱如下: +![](media/te.jpg) +- 项目经理/技术经理、技术专家/架构师:高级程序员再往前发展,会有两个方向。走管理会成为项目经理或者技术经理,前者主要负责项目进度把控、协调沟通等,后者则除了项目管理之外兼具团队管理的职责。能力图谱如下: +![](media/tm.jpg) + 这里需要强调的是,不同于管理路线,如果走技术路线,会发展为资深程序员、技术专家、架构师,能够把控一个系统的整体架构或者精通某一领域技术,做好技术选型和系统设计工作或者为技术团队提供自己精通领域技术的指导咨询工作。后续也会在技术路线上不断加深或者转型为管理岗。 +- 部门经理/技术总监:这一层级的部门经理和技术总监已经基本脱离了程序员的含义,都不再是单纯的技术工作,管理占了日常工作的大部分。相比起之前,更需要的是对团队整体的把控,包括做事、带人、看方向等。能力图谱如下: +![](media/td.jpg) +- CTO: 这一层级是程序员最顶级的职位。其需要的能力包括资源整合规划能力、技术战略规划能力、领导艺术、企业文化和制度建设能力。其中资源整合包括技术资源整合、知识整合、自我行销、人际关系整合。如下: +![](media/cto-model.png) + 能力图谱如下: +![](media/cto.jpg) + +一步步走到金字塔顶部需要不断的学习和进步,包括正确的态度、正确的方法以及持续的努力。本文所述只是笔者自己的体会,也是自己一直在践行的东西。除此之外,肯定还有很多其他优秀的方法和思想能够促进这个过程。 + -一步步走到金字塔顶部需要不断的学习和进步,包括正确的态度、正确的方法以及持续的努力。本文所述只是笔者自己的体会,也是自己一直在践行的东西。除此之外,肯定还有很多其他优秀的方法和思想能够促进这个过程。 \ No newline at end of file diff --git a/book/chapter1-servertech/media/cto-model.png b/book/chapter1-servertech/media/cto-model.png new file mode 100644 index 0000000..a91d676 Binary files /dev/null and b/book/chapter1-servertech/media/cto-model.png differ diff --git a/book/chapter1-servertech/media/cto.jpg b/book/chapter1-servertech/media/cto.jpg new file mode 100644 index 0000000..93f975c Binary files /dev/null and b/book/chapter1-servertech/media/cto.jpg differ diff --git a/book/chapter1-servertech/media/programmer-pyramid.png b/book/chapter1-servertech/media/programmer-pyramid.png index 9e0e1f6..6df8c96 100644 Binary files a/book/chapter1-servertech/media/programmer-pyramid.png and b/book/chapter1-servertech/media/programmer-pyramid.png differ diff --git a/book/chapter1-servertech/media/study.png b/book/chapter1-servertech/media/study.png new file mode 100644 index 0000000..d79e69e Binary files /dev/null and b/book/chapter1-servertech/media/study.png differ diff --git a/book/chapter1-servertech/media/td.jpg b/book/chapter1-servertech/media/td.jpg new file mode 100644 index 0000000..3708f36 Binary files /dev/null and b/book/chapter1-servertech/media/td.jpg differ diff --git a/book/chapter1-servertech/media/te.jpg b/book/chapter1-servertech/media/te.jpg new file mode 100644 index 0000000..6284ab0 Binary files /dev/null and b/book/chapter1-servertech/media/te.jpg differ diff --git a/book/chapter1-servertech/media/tech-tree.png b/book/chapter1-servertech/media/tech-tree.png index 74463f3..993bc6d 100644 Binary files a/book/chapter1-servertech/media/tech-tree.png and b/book/chapter1-servertech/media/tech-tree.png differ diff --git a/book/chapter1-servertech/media/tm.jpg b/book/chapter1-servertech/media/tm.jpg new file mode 100644 index 0000000..bac1210 Binary files /dev/null and b/book/chapter1-servertech/media/tm.jpg differ diff --git a/book/chapter1-servertech/server-tech-tree.md b/book/chapter1-servertech/server-tech-tree.md index 672e682..808f56f 100644 --- a/book/chapter1-servertech/server-tech-tree.md +++ b/book/chapter1-servertech/server-tech-tree.md @@ -68,7 +68,7 @@ - 算法: 经典的排序和查找算法在平时的开发工作中经常会用到,如:冒泡排序、插入排序、选择排序、归并排序、快速排序、希尔排序、堆排序以及二分查找等。此外,在函数/方法的算法实现中要注意递归和迭代各自的优缺点。而衡量算法性能无外乎空间复杂度和时间复杂度。 - 业务相关算法:除了上面的基本算法之外,业务中还会经常涉及到一些更为复杂的算法,如:压缩算法、LRU缓存算法、缓存一致性、编译原理中的状态机等。此外,目前越来越火的机器学习中有很多算法也是在很多业务场景中有很大用途的,如:用于文本分词的结巴分词和中科院ICTCLAS;用于关键词提取的TF-IDF和TextRank;用于计算文本相似度的主题模型、Word2Vec、余弦相似度以及欧几里得距离;用于文本分类的朴素贝叶斯;用于推荐的聚类、协同过滤、用户画像、隐语义模型等。 - 计算机网络: TCP/IP协议是网络最根本的协议,其七层/四层协议栈的设计都是非常精华的东西,连接的建立、断开以及连接的各种状态的转换都是排查、解决网络问题的根本依据。从TCP/IP往上,HTTP协议是现在绝大多数后端应用对外提供的协议,发展到现在已经将要步入HTTP2.0时代,带来了持久连接、连接复用等令人振奋的新特性。此外,基于HTTP的HTTPS协议由于其安全性在逐渐的成为后端服务对外开放的主流协议。业务层面,基于HTTP协议的RESTful规范正成为对外接口的主流规范,而OAuth2.0协议也在成为开放平台对外的主流协议。除了HTTP之外,SMTP是另一个基于TCP/IP的应用协议,主要用在发送邮件上。 -- 设计模式: 在软件开发中,前人的经验形成了很多经典设计模式供我们使用,能够使得软件的实现可服用、可扩展、可维护。经典的工厂模式、简单工厂模式、单例模式、观察者模式、代理模式、建筑者模式、门面模式、适配器模式、装饰器模式在日常的很多开发场景下都具有很重要的意义。 +- 设计模式: 在软件开发中,前人的经验形成了很多经典设计模式供我们使用,能够使得软件的实现可复用、可扩展、可维护。经典的工厂模式、简单工厂模式、单例模式、观察者模式、代理模式、建筑者模式、门面模式、适配器模式、装饰器模式在日常的很多开发场景下都具有很重要的意义。 ## 1.2.7 数据 @@ -135,22 +135,7 @@ 此外,从分布式系统分层模型来看,LVS+Nginx部分是接入层,Tomcat部分是逻辑层,Redis+MySQL则是数据存储层。 -## 1.2.11 架构技能 - -架构设计是很多研发工程师眼里一个比较高大上的工作,很多人的职业理想也是成为架构师。那什么是架构,具有哪些能力才能成为架构师呢? - -引用一句从网上看到的对架构师的比较形象的描述(无法找到出处):架构师的工作,就是在黑暗与混乱里寻找形状,并制造某种色彩。架构的主要工作就是在众多技术、组件中能够识别每一种技术的优劣点、适用场景,从而做出理性的判断和取舍(程序设计的工作主要是逻辑和实现),将构建和维护系统需要的人力成本降到最低(《Clean Architecture》一书对于软件架构的目的描述)。因此,要成为一名合格的架构师,一般来说需要具有以下能力: - -- 基础技术能力:精通某种技术,能够从本质上理解并能够触类旁通。架构师需要具有良好的性能优化能力,能够在应用上线前预估到可能出现的性能问题并给出解决方案;架构师需要具有扎实的调试能力,不管是在代码开发阶段还是线上环境,在应用发生bug或者线上故障时,能够快速识别问题并解决。 -- 设计能力:能够识别问题的本质,区分来自需求方的需求是伪需求还是真正的需求,避免把解决方案当做问题;精通设计模式和各种技术架构,但又不滥用;具有解耦系统的能力,使得系统之间松耦合,原来只能串行的任务可以并行开展。 -- 技术选型能力:平等对待所有技术,只有合适与不合适,没有喜欢与不喜欢;了解主流技术的优缺点,能够辨别是否需要造轮子。 -- 技术前瞻能力:能够预料到需求可能产生怎样的变化,做好前瞻性设计;能清楚地知道系统的瓶颈在什么地方,不断地定位技术难度、研发进度、性能、内存等各方面的瓶颈,不断调整骨干力量解决瓶颈,在风险爆发之前就消除隐患。 -- 沟通能力:架构师需要和业务的各种角色打交道,从而能够在充分理解业务的基础上做好架构设计,因此如何与各种角色的同事做好沟通、理解各方的诉求是一个非常关键的能力。 -- 管理能力:此能力在笔者看来并非是架构师必须的,但确实是能够锦上添花的,包括项目管理和团队管理。对于前者,需要合理划分系统模块、知人善用、有效沟通、控制成本、合理把握进度;对于后者,则需要多倾听团队成员的意见、主动承担责任、多站在成员角度考虑问题、为成员的成长负责。 - -此外,附录F给出了做架构设计的简明指南供参考。 - -## 1.2.12 后端技术体系 +## 1.2.11 后端技术团队体系 对于一个以业务为主的后端技术团队,随着业务的发展会逐渐形成三个主要技术体系。如下: diff --git a/book/chapter2-project/media/15020897352634.jpg b/book/chapter2-project/media/15020897352634.jpg deleted file mode 100644 index 9a1c209..0000000 Binary files a/book/chapter2-project/media/15020897352634.jpg and /dev/null differ diff --git a/book/chapter2-project/media/15020953343968.jpg b/book/chapter2-project/media/15020953343968.jpg deleted file mode 100644 index 47ce848..0000000 Binary files a/book/chapter2-project/media/15020953343968.jpg and /dev/null differ diff --git a/book/chapter2-project/media/add-commit.png b/book/chapter2-project/media/add-commit.png deleted file mode 100644 index 60ebb77..0000000 Binary files a/book/chapter2-project/media/add-commit.png and /dev/null differ diff --git a/book/chapter2-project/media/gitblit.png b/book/chapter2-project/media/gitblit.png deleted file mode 100644 index 3bd3f51..0000000 Binary files a/book/chapter2-project/media/gitblit.png and /dev/null differ diff --git a/book/chapter2-project/media/gitflow.png b/book/chapter2-project/media/gitflow.png index d3ceef3..c00a64c 100644 Binary files a/book/chapter2-project/media/gitflow.png and b/book/chapter2-project/media/gitflow.png differ diff --git a/book/chapter2-project/quality.md b/book/chapter2-project/quality.md index a156630..92805ae 100644 --- a/book/chapter2-project/quality.md +++ b/book/chapter2-project/quality.md @@ -203,7 +203,7 @@ public class AppServiceImplTest { AppServiceImpl service = new AppServiceImpl(); service.setAppDao(mock); App app = service.getAppById(1);//调用测试方法 - assertEquals(expectedApp, App); //断言测试结果 + assertEquals(expectedApp, app); //断言测试结果 Easymock.verify(mock); //验证Mock对象被调用 } @@ -353,7 +353,7 @@ PowerMock的工作过程和EasyMock类似,不同之处在于需要在类层次 - 该方法能否用一个main方法就能直接运行。 - 该方法的参数能否不依赖外部环境而进行自由模拟。 -对于一些将访问代码糅杂在业务逻辑里无法单元测试的方法,可以通过将需要的数据从上下文中抽离出来达到不依赖外部环境的目的,如: +对于一些将访问代码(简单的顺序调用,一般需要和上下文打交道)和业务逻辑糅杂在一起、多线程、异步等很难进行单元测试的方法,可以使用谦卑对象模式,通过将需要的数据从上下文中抽离出来达到不依赖外部环境的目的,如: ``` @Resource @@ -368,12 +368,19 @@ public void methodA(){ 可以重构为 ``` -public void methodA(User u){ - ….//业务逻辑 +public void methodA(){ + User u = userDao.getById(1); + methodA_1(u); +} + +public void methodA_1(User u){ + + ….//业务逻辑 + } ``` -这样,可以自由模拟User数据,来对此方法做单元测试。 +这样,可以自由模拟User数据,来对抽离出的方法methodA_1做单元测试。 此外,编写单元测试还需要注意: @@ -430,12 +437,26 @@ public class MyClassTest { 1. 变量命名按照Java通用方式Camel命名法。常量尤其注意使用全大写_下划线连接单词的方式,如 USER_KEY。 1. 变量和类命名务必具有意义,能让人一眼看出表示的意思。如userList表示用户列表,不要使用list、set、button这种无意义的命名方式。 -1. 数据库的一个表对应一个领域类,以entity、domain或者meta做为包名都可以。 -1. 数据访问层命名形如xxxDao,这里应该封装所有与数据库层相关的东西,如表名、各个列等。 -1. service类是封装业务逻辑的类,其中的方法要和此业务逻辑相关。比如UserService就是和User相关的业务方法。 -1. 当一个东西具有缓存和实际值两种的时候,务必保证存储和获取的接口只有一个。比如获取用户的level,应该只在UserLevelService中暴露一个接口提供 -1. 当一个方法的主逻辑代码超过30行,务必考虑封装方法或者类。 +1. 当一个方法的主逻辑代码超过30行(经验值),务必考虑封装方法或者类。 1. 类内方法定义的顺序依照开发者的关心程度和承载的信息价值依次是:公有方法或保护方法、私有方法、getter/setter方法。 +1. 数据领域类POJO的字段 + - "不会为空"时使用基本数据类型,需要注意字段默认值不能具有特殊意义。 + - "可为空"时使用包装类型,需要做好空值判断。 +1. 方法尤其是对外提供的接口(RPC、SDK)如果返回值可能为空,务必使用Optional包装返回值,否则认为返回值不会为空。 +1. “异常”编码规范 + - 优先考虑使用错误码返回值而不是异常表示错误:用错误码返回值处理可能会发生的事情,用异常捕捉处理不期望发生的事情 + - 在抛出异常时,需要尽可能精确地描述问题和相关信息。如:尽可能的使用最具体的异常来声明方法,这样才能使得代码更容易理解。 + - 当在方法上声明抛出异常时,需要进行文档说明。在Javadoc中加入throws声明,描述抛出异常的场景。 + - 不要捕获Throwable。 + - 不要忽略异常,至少要记录异常的信息。 + - 不要记录并抛出异常。仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。 + - 包装异常时不要抛弃原始的异常。一定要把原始的异常设置为cause(Exception有构造方法可以传入cause)。 +1. MVC规范 + - 数据库的一个表对应一个领域类,以entity、domain或者meta做为包名都可以。 + - 数据访问层命名形如xxxDao,这里应该封装所有与数据库层相关的东西,如表名、各个列等。 + - controller或者servlet中不要掺杂复杂业务逻辑代码(放到service中) + - service类是封装业务逻辑的类,其中的方法要和此业务逻辑相关。比如UserService就是和User相关的业务方法。 + - 当一个东西具有缓存和实际值两种的时候,务必保证存储和获取的接口只有一个。比如获取用户的level,应该只在UserLevelService中暴露一个接口提供 对于这些规范,可以使用CheckStyle、PMD、FindBugs等工具进行保证,使用相关的Maven插件即可。此外,阿里巴巴也推出了其Java编码规范对应的Intellij插件。 @@ -473,7 +494,6 @@ public class MyClassTest { 1. 代码的可复用:代码的可复用是优秀工程师的一个本能。但切记不能为了复用而复用。 1. 程序需要状态,对象不需要状态,如果对象有了状态,就会引发多线程问题。程序的状态应该统一由数据库、缓存、任务队列这些外部容器来容纳,处理时以局部变量的形式在线程内部流转直至被回收。 1. 在做商业计算时(金融、电商交易),务必要注意选择的数据类型的存储数值范围,防止用户传入的数值溢出造成归零或者负值,从而造成交易损失。比如直播平台用户送礼物的场景,设计的接口的参数为礼物ID和礼物数目。那么如果使用int类型计算总的花费,当礼物数目非常大的时候就会造成花费超出int的存储范围而溢出为<=0的值,使得用户可以无限送礼物。 -2. 优先考虑使用返回值而不是异常表示错误。一般用错误码返回值处理可能会发生的事情,用异常捕捉处理不期望发生的事情。 1. 编码时,在每次调用一个方法时多考虑以下问题: - 会不会出现空指针? diff --git a/book/chapter2-project/vcs.md b/book/chapter2-project/vcs.md index de0208e..b1a3cdb 100644 --- a/book/chapter2-project/vcs.md +++ b/book/chapter2-project/vcs.md @@ -27,7 +27,7 @@ SVN版本:1.10.0 ### 流程与常用命令 -![](media/15020897352634.jpg) +![](media/svn-process.png) 一个通常较简单的SVN工作流程如上图所示。 @@ -211,39 +211,48 @@ ssh-keygen -t rsa -C [userName] 按照官方文档搭建一个Git服务器是比较繁琐的。面对这些复杂的操作步骤以及Git本身上手的门槛,很多人望而却步,造成了之前Git并没有那么普及的状况。而一切从Github横空出世开始改变了,这个看似简单的网站给很多人带来了简单极致方便的代码托管服务,同时也给全世界带来了一股开源的风潮。很多初创公司或者小公司直接选择使用Github建立自己的代码仓库。 -与此同时,许多Github的开源实现层出不穷,如今你只需要下载一个类似的实现部署到服务器上,就能拥有自己的“Github”。 +与此同时,许多GitHub的开源实现层出不穷,如今你只需要下载一个类似的实现部署到服务器上,就能拥有自己的“GitHub”。 目前比较知名、用的较多的Github实现,有以下几个: -- [Gitlab](https://about.gitlab.com/): Ruby on rails实现,是目前最为出名也最为强大的github克隆实现,并且除了版本管理之外,集成了项目管理、持续集成等等很多功能。官网提供了很多操作系统下的一键安装包。遗憾的是,Gitlab现在已经商业化,一些强大的功能只有在其收费版本中才能体验到。 +- [GitLab](https://about.gitlab.com/): Ruby on rails实现,是目前最为出名也最为强大的GitHub克隆实现,并且除了版本管理之外,集成了项目管理、持续集成等等很多功能。官网提供了很多操作系统下的一键安装包。遗憾的是,GitLab现在已经商业化,一些强大的功能只有在其收费版本中才能体验到。 ![](media/gitlab.png) -- [Gitbucket](https://github.com/gitbucket/gitbucket):基于Scala编写,极易安装,扔一个war包到Tomcat就完成部署,完全可以和其他如Maven、Jenkins并存在一个JavaEE容器中。虽然功能没有Gitlab那么强大,但胜在简单。更重要的是对于Java工程师来说是友好的,有不满意的直接可以做二次开发。 +- [GitBucket](https://github.com/gitbucket/gitbucket):基于Scala编写,极易安装,扔一个war包到Tomcat就完成部署,完全可以和其他如Maven、Jenkins并存在一个JavaEE容器中。虽然功能没有GitLab那么强大,但胜在简单。更重要的是对于Java工程师来说是友好的,有不满意的直接可以做二次开发。 ![](media/gitbucket.png) ### 工作流 -由于Git是分布式版本控制系统,虽然带来了很多优势,但是也同时带来了协同工作的复杂性。因此需要一套基于Git的工作流来规范整个协同流程。目前,比较流行的有以下两种: +由于Git是分布式版本控制系统,虽然带来了很多优势,但是也同时带来了协同工作的复杂性。因此需要一套基于Git的工作流来规范整个协同流程。目前,比较流行的有以下三种: -- Github workflow +- 三驾马车 +- GitHub workflow - GitFlow -#### Github workflow +#### 三驾马车 -Github workflow是基于github一种常用的工作方式,比较简单。流程分为以下几步: +三驾马车模式使用三个分支:开发分支、预发布分支/测试分支以及发布分支。经常用在客户端软件开发中。 + +- 开发分支:所有开发人员提交代码的分支。 +- 预发布分支/测试分支:当开发分支提交的功能达到一定量时,将开发分支合并到预发布分支,预发布分支仅做缺陷修复等与发布相关的工作,不做新功能开发。所有的变动都要合并回开发分支。此分支代码发布测试版本,供测试。 +- 发布分支:当预发布分支的功能测试无误后,合并入发布分支,做为正式发布版本。此分支可以做缺陷修复,变动需要合并回预发布分支和开发分支。 + +#### GitHub workflow + +GitHub workflow是基于GitHub一种常用的工作方式,比较简单。流程分为以下几步: - 检出新的分支。 - 在开发分支上完成开发工作,commmit并push到远程库分支中。 -- 向主分支发起pull request -- 在pull request中发起讨论和修改 +- 向主分支发起Pull Request +- 在Pull Request中发起讨论和修改 - 将开发分支部署测试环境经测试无误后merge回主分支。 -这里最为核心的就是基于pull request的协作方式,在类似于Github这种应用中,还可以基于此来进行code review、任务沟通等工作。 +这里最为核心的就是基于Pull Request的协作方式,在类似于GitHub这种应用中,还可以基于此来进行code review、任务沟通等工作。 #### GitFlow -相比起Github workflow,GitFlow根据Git原来的命令和语义,做了一层语义抽象,多了很多新的定义,从源代码管理角度对通常意义上的软件开发活动进行了约束。流程如下图所示: +相比起GitHub workflow,GitFlow根据Git原来的命令和语义,做了一层语义抽象,多了很多新的定义,从源代码管理角度对通常意义上的软件开发活动进行了约束。流程如下图所示: -![](media/15020953343968.jpg) +![](media/gitflow.png) 其中,GitFlow中定义了两大类分支: diff --git a/book/chapter3-framework/README.md b/book/chapter3-framework/README.md new file mode 100644 index 0000000..95cad3a --- /dev/null +++ b/book/chapter3-framework/README.md @@ -0,0 +1,21 @@ +# 第三章 开发框架 + +说到Java开发,开发框架是逃不开的的一个话题。也是得益于Java生态圈中各种框架的层出不穷,Java的生命力才如此旺盛。在无数次被声称“Java将亡”后依然活跃在开发领域,各种Java框架都功不可没。 + +在Java刚开始出现的时候,写一个简单的Class,配上main方法就能完成一个可运行的程序。然而随着应用规模的越来越大以及参与人数的增多,以往一个类能够完成的功能,现在需要有许多类来完成。这时候如果没有一个开发规范或者说是约定,那么随着功能的增多,也就越来越难以维护。此外,由于开发人员水平的层次不齐,需要一些公共组件来保证一定程度的代码质量。开发框架就是一种基于约定、屏蔽了底层细节、规范了开发流程的技术。 + +在一个可用的Java Web应用中,一般由以下几种框架构成: + +- IOC框架:依赖注入/控制反转,即将依赖从代码层面转移到了容器配置层面。在Java中是使用的最为普遍的框架。 +- ORM框架:对象关系映射,即将数据库的表映射到Java中的对象的一种数据库操作框架。 +- LOG框架:日志框架。即记录应用运行、异常日志的框架。 +- Web框架:一般指的是model-view-controller的分层Web开发框架,将业务代码做了逻辑分层,各司其职,能够做到灵活的配置和扩展。 + +其层次结构如下图所示: + +![](media/web-framework.jpg) + +如图,除了上述的四种框架,还有安全框架、AOP、缓存操作框架也是关键的开发框架。 + +这里需要说明的一点是,虽然使用开发框架有很多好处,但任何事情都不会是绝对的优劣,都是一种权衡利弊后做出的取舍。就MVC框架来说,当提供的业务接口需要特别高的并发的时候,可以考虑不使用框架,直接使用Servlet处理请求并直接输出流数据(不走视图解析引擎和JSON等序列化)。毕竟请求经过框架的一层层调用性能肯定会受到影响。其他的诸如ORM框架也类似。 + diff --git a/book/chapter3-framework/end.md b/book/chapter3-framework/end.md new file mode 100644 index 0000000..bbe9e7d --- /dev/null +++ b/book/chapter3-framework/end.md @@ -0,0 +1,11 @@ +# 总结 + +## 学习资料 + +- Spring: [跟开涛学Spring3](http://www.open-open.com/doc/view/5407635b943d410c9cfde409c90450b7) +- Spring MVC: [跟开涛学SpringMvc](http://www.cnblogs.com/kaitao/archive/2012/07/16/2593441.html) +- MyBatis: [MyBatis实战教程](http://www.yihaomen.com/article/java/302.htm) [MyBatis学习](http://limingnihao.iteye.com/blog/781671) + +对于这些框架或者是一些常用的软件,个人最推崇的还是阅读**官方文档**来学习。当然,看这些资料能让你入门地更加快速一些。 + + diff --git a/book/chapter3-framework/ioc.md b/book/chapter3-framework/ioc.md new file mode 100644 index 0000000..dff9e2e --- /dev/null +++ b/book/chapter3-framework/ioc.md @@ -0,0 +1,337 @@ +# 3.1 依赖注入 + +IOC,控制翻转(Inversion of Control),又叫依赖注入(Dependency Inject)。即将代码里对象之间的依赖关系转移到容器中,这样就能够很灵活的通过面向接口的编程方式改变真正的实现类。 + +如下是用代码来维护依赖关系的,如果此时后面有了另外的IUser的实现类,那么如果要使用这个新的实现类则需要修改代码重新set。 + +``` +public interface IUser{ + void say(); +} + +public class AdminUser implements IUser{ + public void say(){ + System.out.println("I'm admin"); + } +} + +public class IOCTest{ + private IUser user; + + public void setUser(IUser user){ + this.user = user; + } + + public IUser getUser(){ + return this.user; + } + + public void test(){ + this.user.say(); + } + + public static void main(String[] args){ + IOCTest test = new IOCTest(); + test.setUser(new AdminUser()); + + test.test(); + } +} +``` +而IOC的作用就是讲这些依赖关系的维护从代码里拿出来,通过容器来维护这些关系。一个典型的例子,伪代码如下: + +``` +IOCContainer container = new IOCContainer("..");//可以通过一个上下文配置文件,也可以通过扫描注解 + +IOCTest iocTest = container.getInstance("iocTest"); +iocTes.test(); +``` + +一个典型的上下文配置文件如下: + +``` + + + + +``` + +这样当想要改变实现类时只需要修改配置文件即可。 + +对于依赖注入,JSR330(Dependency Injection for Java)做了一些规范。该规范主要是面向依赖注入使用者,而对注入器实现、配置并未作详细要求。目前Spring、Guice已经开始兼容该规范。JSR-330规范并未按JSR惯例发布规范文档,只发布了API源码。其指定了获取对象的一种方法,该方法与构造器、工厂以及服务定位器(例如 JNDI)这些传统方法相比可以获得更好的可重用性、可测试性以及可维护性。此方法的处理过程就是依赖注入。 + +目前市面上常见的IOC框架有以下几个: + +- Google Guice +- PicoContainer +- Dagger +- SpringFramework + +其中,兼容JSR330标准的Guice易用性最好;Pico比较轻量,不过需要手工添加Bean类到容器,用起来有点烦;Dagger使用注解处理工具,其性能非常好,是一种很有前途的DI方案;SpringFramework历史非常悠久,有自己的一套依赖注入体系, 依赖于Spring强大的生态是目前用的最广泛的依赖注入框架,目前已经兼容JSR330规范。 + +此外,基本所有的IOC框架都支持构造器注入、setter注入以及字段注入三种方式。 + +## 3.1.1 JSR330 + +如果要使用JSR330提供的注解等功能。可引入依赖: + +``` + + javax.inject + javax.inject + 1 + +``` + +主要提供了以下几个注解和类: + +1. @Inject + + 注解@Inject标识了可注入的构造器、方法或字段。可以用于静态或实例成员。一个可注入的成员可以被任何访问修饰符(private、package-private、protected、public)修饰。注入顺序为构造器、字段、方法。超类的字段、方法将优先于子类的字段、方法被注入。对于同一个类的字段是不区分注入顺序的,同一个类的方法亦同。如: + + ``` + public class IOCTest { + + private IUser user; + + @Inject + public void setUser(IUser user) { + this.user= user; + } + } + ``` + +1. @Qualifier + + @Qualifier是一个元注解,用来构建自定义限定符,任何人都可以定义新的限定器注解。一个限定器注解如下: + + - 是被@Qualifier、@Retention(RUNTIME)标注的,通常也被@Documented标注。 + - 可以拥有属性。 + - 可能是公共 API 的一部分,就像依赖类型一样,而不像类型实现那样不作为公共 API 的一部分。 + - 如果标注了 @Target 可能会有一些用法限制。本规范只是指定了限定器注解可以被使用在字段和参数上,但一些注入器配置可能使用限定器注解在其他一些地方(例如方法或类)上。 + +1. @Named + + @Named就是使用上面讲的Qualifier的一个限定器注解。可以指定依赖的组件的名称。 + + ``` + public class IOCTest { + + private IUser user; + + @Inject + public void setUser(@Named("adminUser") IUser user) { + this.user= user; + } + } + ``` + + 此外,@Named还可以用做标注一个组件。如 + + ``` + @Named + public class AdminUser implements IUser{ + public void say(){ + System.out.println("I'm admin"). + } + } + ``` + +1. Provider + + 接口Provider用于提供类型T的实列。Provider一般情况是由注入器实现的。对于任何可注入的T而言,都可以注入 Provider。与直接注入T相比,注入 Provider 使得: + + - 可以返回多个实例。 + - 实例的返回可以延迟化或可选 + - 打破循环依赖。 + - 可以在一个已知作用域的实例内查询一个更小作用域内的实例。 + + ``` + public class IOCTest { + + private IUser user; + + @Inject + public void setUser(Provider user) { + this.user= user; + } + } + ``` + +1. @Scope + + 注解 @Scope是一个元注解,用于标识作用域注解。一个作用域注解是被标识在包含一个可注入构造器的类上的,用于控制该类型的实例如何被注入器重用。缺省情况下,如果没有标识作用域注解,注入器将为每一次注入都创建(通过注入类型的构造器)新实例,并不重用已有实例。如果多个线程都能够访问一个作用域内的实例,该实例实现应该是线程安全的。作用域实现由注入器完成。 + +1. @Singleton + + @Singleton是基于Scope注解实现的一个作用域注解。表示注入器只实例化一次的类型。该注解不能被继承。如: + + ``` + @Singleton + public class AdminUser implements IUser{ + public void say(){ + System.out.println("I'm admin"). + } + } + ``` + +## 3.1.2 Guice + +Guice是Google开源的轻量级ioc框架,兼容JSR330规范。其用 Module 來定义所有元件的实际类别。依赖定义部分可以使用JSR330的注解。 + +``` +public class IOCTest { + + private IUser user; + + @Inject + public void setUser(IUser user) { + this.user= user; + } +} + +public class IOCTestModule extends AbstractModule { + @Override + protected void configure() { + bind(IUser.class).to(AdminUser.class); + bind(IOCTest.class).to(IOCTest.class); + } +} + +public static void main(String[] args) { + Injector injector = Guice.createInjector(new IOCTestModule()); + IOCTest test = injector.getInstance(IOCTest.class); + ... +} +``` + +## 3.1.3 PicoContainer + +PicoContainer是一个“微核心”(micro-kernel)的容器,它利用了Inversion of Control模式和Template Method模式,提供面向组件的开发、运行环境。PicoContainer是“极小”的容器,只提供了最基本的特性。其最重要的特性是实例化任意对象。这些通过它的API完成,这些API类似于HashMap。向PicoContainer指定java.lang.Class对象,之后能够获得对象实例。如: + +``` +MutablePicoContainer pico = new DefaultPicoContainer(); + +pico.addComponent(AdminUser.class); //通过class注册 +pico.addComponent(new AdminUser()) //通过type注册 +pico.addComponent(IOCTest.class); + +IUser user = (IUser) pico.getComponent(AdminUser.class); +IOCTest test = (IOCTest)pico.getComponent(IOCTest.class); +``` + +不过目前PicoContainer的发展几乎已经停滞。笔者仅仅在Intellij的插件开发中见过对它的使用。 + +## 3.1.4 Dagger + +Dagger是Google开源的一个框架(早先的版本是由Square创建的,现版本由Google维护),支持Android和Java,现在已经更新到2.0。它使用生成代码实现完整依赖注入的框架(在编译期),极大减少了使用者的编码负担,且其相对于其他大部分IOC框架来说没有使用反射,性能有一定的提升。相对于都出自Google的Guice来说,其更加轻量级,没有Guice中一些相对高级的功能功,如AOP等。 + +Dagger的依赖注入有自己的一些注解配置,如下: + +``` +public class IOCTest { + + private IUser user; + + @Inject + public void setUser(IUser user) { + this.user= user; + } +} + +@Module +public class TestModule { + @Provides IUser provideUser() { + return new AdminUser(); + } +} + +@Component(modules = TestModule.class) +public interface TestComponent { + void inject(IOCTest test); +} +``` + +使用如下: + +``` +IOCTest test = new IOCTest(); +TestComponent component = DaggerActivityComponent.builder().activityModule(new TestModule()).build(); +component.inject(test); +test.test(); +... +``` + +## 3.1.5 Spring Framework + +Spring的IOC应该是Java开发中最为常用的功能之一。其XML配置的一个例子如下: + +``` + + + + + + + + +``` + +使用代码: + +``` +ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xm"); +IOCTest iocTest = context.getBean(IOCTest.class); +iocTes.test(); +``` + +此外,从Spring2.0开始引入了对注解的支持,并且后来逐步兼容了JSR330规范。这里简单对比以下Spring自身的依赖注入注解与JSR330。 + +Spring | Jsr330 | 备注 +----|-----|------|---- +@Autowired | @Inject | @Inject注解没有required属性 +@Component | @Named | JSR_330标准并没有提供复合的模型,只有一种方式来识别组件 +@Scope(“singleton”) | @Singleton | JSR-330默认的作用域类似Spring的prototype,而Spring默认是单例的。如果要使用非单例的作用域,开发者应该使用Spring的@Scope注解。java.inject也提供一个@Scope注解,然而,这个注解仅仅可以用来创建自定义的作用域时才能使用。 +@Qualifier | @Qualifier/@Named | javax.inject.Qualifier仅仅是一个元注解,用来构建自定义限定符的。而String的@Qualifier等限定符可以通过javax.inject.Named来实现 + +除上述的注解外,Spring还有注入@Value、@Required等注解,这在JSR330中都没有对应的东西。 + +这里需要补充的一点是,Spring的IOC目前早已支持JSR250(common annotations)中提供的注解:Resource、PostConstruct、PreDestroy。这里和Spring自带的注解做一下对比。 + +Spring | JSR250 | 备注 +----|-----|------|---- +@Autowired | @Resource | @Resource是先根据Bean的名称去匹配Bean,获取不到的话再根据类型去匹配;而@Autowired则是根据类型匹配,通过名称则需要Spring的@Qualifier注解的配合。 +@PostContruct | init-method | Spring中的XML配置中的init-method可以有同样的作用,即在Bean构造完后做一些初始化动作。@PostContruct具有更高优先级,同时存在的话会先执行。 +@PreDesroy | destroy-method | Spring中的XML配置中的destroy-method可以有同样的作用,即在Bean销毁前做一些收尾工作。@PreDesroy注解具有更高优先级,同时存在的话会先执行。 + +这里需要说明的是,Spring对JSR250的支持的实现是在org.springframework.context.annotation.CommonAnnotationBeanPostProcessor此类中。 + +## 3.1.6 循环依赖 + +IOC中一个常见的问题就是循环依赖,如下: + +``` +class TestA { + @Inject TestB b; +} + +class TestB { + @Inject TestC c; +} + +class TestC { + @Inject TestA a; +} +``` + +以上几个IOC框架,对于循环的处理: + +- 通过替换为Provider,并且在构造器或者方法中调用Provider的get方法来打破循环依赖 +- 如果不依赖于Provider,对于构造器中的循环依赖是无法解决的,会抛出异常。 +- 对于方法或字段注入的情况,将其依赖的一边放置到单例作用域中(可以缓存),可以使得循环依赖能够被注入器解析。 + diff --git a/book/chapter3-framework/log.md b/book/chapter3-framework/log.md new file mode 100644 index 0000000..a2cf2d1 --- /dev/null +++ b/book/chapter3-framework/log.md @@ -0,0 +1,651 @@ +# 3.3 日志 + +日志在应用开发中是一个非常关键的部分。有经验的工程师能够凭借以往的经验判断出哪里该打印日志、该以何种级别打印日志。这样就能够在线上发生问题的时候快速定位并解决问题,极大的减少应用的运维成本。 + +使用控制台输出其实也算日志的一种,在容器中会打印到容器的日志文件中。但是,控制台输出过于简单,缺乏日志中级别控制、异步、缓冲等特性,因此在开发中要杜绝使用控制台输出作为日志(System.out.println)。而Java中已经有很多成熟的日志框架供大家使用: + +- JDK Logging +- Apache Log4j +- Apache Log4j2 +- Logback + +此外,还有两个用于实现日志统一的框架:Apache Commons-Logging、SLF4j。与上述框架的不同之处在于,其只是一个门面,并没有日志框架的具体实现,可以认为是日志接口框架。 + +对于这些日志框架来说,一般会解决日志中的以下问题: + +- 日志的级别: 定义日志级别来区分不同级别日志的输出路径、形式等,帮助我们适应从开发调试到部署上线等不同阶段对日志输出粒度的不同需求。 +- 日志的输出目的地:包括控制台、文件、GUI组件,甚至是套接口服务器、UNIX Syslog守护进程等。 +- 日志的输出格式:日志的输出格式(JSON、XML)。 +- 日志的输出优化:缓存、异步等。 + +这里需要说的是,目前有几个框架提供了占位符的日志输出方式,然而其最终是用indexOf去循环查找再对信息进行拼接的,会消耗CPU。建议使用正确估算大小的StringBuilder拼装输出信息,除非是实在无法确定日志是否输出才用占位符。 + +## 3.3.1 JDK Logging + +JDK Logging就是JDK自带的日志操作类,在java.util.logging包下面,通常被简称为JUL。 + +### 配置 + +JDK Logging配置文件默认位于$JAVA_HOME/jre/lib/logging.properties中,可以使用系统属性java.util.logging.config.file指定相应的配置文件对默认的配置文件进行覆盖。 + +``` +handlers= java.util.logging.FileHandler,java.util.logging.ConsoleHandler +.handlers = java.util.logging.FileHandler,java.util.logging.ConsoleHandler #rootLogger使用的Handler +.level= INFO #rootLogger的日志级别 + +##以下是FileHandler的配置 +java.util.logging.FileHandler.pattern = %h/java%u.log +java.util.logging.FileHandler.limit = 50000 +java.util.logging.FileHandler.count = 1 +java.util.logging.FileHandler.formatter =java.util.logging.XMLFormatter #配置相应的日志Formatter。 + +##以下是ConsoleHandler的配置 +java.util.logging.ConsoleHandler.level = INFO +java.util.logging.ConsoleHandler.formatter =java.util.logging.SimpleFormatter #配置相应的日志Formatter。 + +#针对具体的某个logger的日志级别配置 +me.rowkey.pje.log.level = SEVERE + +#设置此logger不会继承成上一级logger的配置 +me.rokey.pje.log.logger.useParentHandlers = false +``` + +这里需要说明的是logger默认是继承的,如me.rowkey.pje.log的logger会继承me.rowkey.pje的logger配置,可以对logger配置handler和useParentHandlers(默认是为true)属性, 其中useParentHandler表示是否继承父logger的配置。 + +JDK Logging的日志级别比较多,从高到低为:OFF(2^31-1)—>SEVERE(1000)—>WARNING(900)—>INFO(800)—>CONFIG(700)—>FINE(500)—>FINER(400)—>FINEST(300)—>ALL(-2^31)。 + +### 使用 + +JDK Logging的使用非常简单: + +``` +public class LoggerTest{ + + private static final Logger LOGGER = Logger.getLogger(xx.class.getName()); + + public static void main(String[] args){ + LOGGER.info("logger info"); + } +} +... +``` + +### 性能优化 + +JDK Logging是一个比较简单的日志框架,并没有提供异步、缓冲等优化手段。也不建议大家使用此框架。 + +## 3.3.2 Log4j + +Log4j应该是目前Java开发中用的最为广泛的日志框架。 + +### 配置 + +Log4j支持XML、Proerties配置,通常还是使用Properties: + +``` +root_log_dir=${catalina.base}/logs/app/ + +# 设置rootLogger的日志级别以及appender +log4j.rootLogger=INFO,default + +# 设置Spring Web的日志级别 +log4j.logger.org.springframework.web = ERROR + +# 设置default appender为控制台输出 +log4j.appender.default=org.apache.log4j.ConsoleAppender +log4j.appender.default.layout=org.apache.log4j.PatternLayout +log4j.appender.default.layout.ConversionPattern=[%-d{HH\:mm\:ss} %-3r %-5p %l] >> %m (%t)%n + +# 设置新的logger,在程序中使用Logger.get("myLogger")即可使用 +log4j.logger.myLogger=INFO,A2 + +# 设置另一个appender为按照日期轮转的文件输出 +log4j.appender.A2=org.apache.log4j.DailyRollingFileAppender +log4j.appender.A2.File=${root_log_dir}log.txt +log4j.appender.A2.Append=true +log4j.appender.A2.DatePattern= yyyyMMdd'.txt' +log4j.appender.A2.layout=org.apache.log4j.PatternLayout +log4j.appender.A2.layout.ConversionPattern=[%-d{HH\:mm\:ss} %-3r %-5p %l] >> %m (%t)%n + +log4j.logger.myLogger1 = INFO,A3 + +# 设置另一个appender为RollingFileAppender,能够限制日志文件个数 +log4j.appender.A3 = org.apache.log4j.RollingFileAppender +log4j.appender.A3.Append = true +log4j.appender.A3.BufferedIO = false +log4j.appender.dA3.File = /home/popo/tomcat-yixin-pa/logs/pa.log +log4j.appender.A3.Encoding = UTF-8 +log4j.appender.A3.layout = org.apache.log4j.PatternLayout +log4j.appender.A3.layout.ConversionPattern = [%-5p]%d{ISO8601}, [Class]%-c{1}, %m%n +log4j.appender.A3.MaxBackupIndex = 3 #最大文件个数 +log4j.appender.A3.MaxFileSize = 1024MB +``` + +如果Log4j文件不直接在classpath下的话,可以使用PropertyConfigurator来进行配置: + +``` +PropertyConfigurator.configure("..."); + +``` + +Log4j的日志级别相对于JDK Logging来说,简化了一些:DEBUG < INFO < WARN < ERROR < FATAL。 + +这里的logger默认是会继承父Logger的配置(rootLogger是所有logger的父logger),如上面myLogger的输出会同时在控制台和文件中出现。如果不想这样,那么只需要如下设置: + +``` +log4j.additivity.myLogger=false +``` + +### 使用 + +程序中对于Log4j的使用也非常简单: + +``` +import org.apache.log4j.Logger; + + +private static final Logger LOGGER = Logger.getLogger(xx.class.getName()); +... +LOGGER.info("logger info"); +... +``` + +这里需要注意的是,虽然Log4j可以根据配置文件中日志级别的不同做不同的输出,但由于字符串创建或者拼接也是耗资源的,因此,下面的用法是不合理的。 + +``` +LOGGER.debug("..."); +``` + +合理的做法应该是首先判断当前的日志级别是什么,再去做相应的输出,如: + +``` +if(LOGGER.isDebugEnabled()){ + LOGGER.debug("..."); +} +``` +当然,如果是必须输出的日志可以不做此判断,比如catch异常打印错误日志的地方。 + +### 性能优化 + +Log4j为了应对某一时间里大量的日志信息进入Appender的问题提供了缓冲来进一步优化性能: + +``` +log4j.appender.A3.BufferedIO=true +#Buffer单位为字节,默认是8K,IO BLOCK大小默认也是8K +log4j.appender.A3.BufferSize=8192 +``` + +以上表示当日志内容达到8k时,才会将日志输出到日志输出目的地。 + +除了缓冲以外,Log4j还提供了AsyncAppender来做异步日志。但是AsyncAppender只能够通过xml配置使用: + +``` + + + + + + + + + + + + + + +``` + +## 3.3.3 Log4j2 + +2015年8月,官方正式宣布Log4j 1.x系列生命终结,推荐大家升级到Log4j2,并号称在修正了Logback固有的架构问题的同时,改进了许多Logback所具有的功能。Log4j2与Log4j1发生了很大的变化,并不兼容。并且Log4j2不仅仅提供了日志的实现,也提供了门面,目的是统一日志框架。其主要包含两部分: + +- log4j-api: 作为日志接口层,用于统一底层日志系统 +- log4j-core : 作为上述日志接口的实现,是一个实际的日志框架 + +### 配置 + +Log4j2的配置方式只支持XML、JSON以及YAML,不再支持Properties文件,其配置文件的加载顺序如下: + +- log4j2-test.json/log4j2-test.jsn +- log4j2-test.xml +- log4j2.json/log4j2.jsn文件 +- log4j2.xml + +如果想要自定义配置文件位置,需要设置系统属性log4j.configurationFile。 + +``` +System.setProperty("log4j.configurationFile", "..."); +或者 +-Dlog4j.configurationFile="xx" +``` + +配置文件示例: + +``` + + + + + + + + + + %d %p %C{1.} [%t] %m%n + + + + + + + + + + + + + + + + + + +``` + +上面的monitorInterval使得配置变动能够被实时监测并更新,且能够在配置发生改变时不会丢失任何日志事件;additivity和Log4j一样也是为了让Looger不继承父Logger的配置;Configuration中的status用于设置Log4j2自身内部的信息输出,当设置成trace时,你会看到Log4j2内部各种详细输出。 + +Log4j2在日志级别方面也有了一些改动:TRACE < DEBUG < INFO < WARN < ERROR < FATAL, 并且能够很简单的自定义自己的日志级别。 + +``` + + + + +``` + +上面的intLevel值是为了与默认提供的标准级别进行对照的。 + +### 使用 + +使用方式也很简单: + +``` +private static final Logger LOGGER = LogManager.getLogger(xx.class); + +LOGGER.debug("log4j debug message"); +``` + +这里需要注意的是其中的Logger是log4j-api中定义的接口,而Log4j1中的Logger则是类。 + +相比起之前我们需要先判断日志级别,再输出日志,Log4j2提供了占位符功能: + +``` +LOGGER.debug("error: {} ", e.getMessage()); +``` + +### 性能优化 + +在性能方面,Log4j2引入了基于LMAX的Disruptor的无锁异步日志实现进一步提升异步日志的性能: + +``` + + + +``` + +需要注意的是,由于默认日志位置信息并没有被传给异步Logger的I/O线程,因此这里的includeLocation必须要设置为true。 + +和Log4j一样,Log4j2也提供了缓冲配置来优化日志输出性能。 + +``` + + + + %d %p %C{1.} [%t] %m%n + + + +``` + +## 3.3.4 Logback + +Logback是由Log4j创始人设计的又一个开源日志组件,相对Log4j而言,在各个方面都有了很大改进。 + +Logback当前分成三个模块: + +- logback-core是其它两个模块的基础模块。 +- logback-classic是Log4j的一个改良版本。logback-classic完整实现SLF4J API使你可以很方便地更换成其它日志系统如Log4j或JDK Logging。 +- logback-access访问模块与Servlet容器集成提供通过HTTP来访问日志的功能。 + +### 配置 + +Logback的配置文件如下: + +``` + + + + + + + + ${root_log_dir}app.log + true + + %date [%level] [%thread] %logger{80} [%file : %line] %msg%n + + + ${root_log_dir}app.log.%d{yyyy-MM-dd}.%i + 30 #只保留最近30天的日志文件 + #每天的日志按照100MB分割 + 100MB + + 20GB#日志总的大小上限,超过此值则异步删除旧的日志 + + + + + ${root_log_dir}mylog.log + true + + %date [%level] [%thread] %logger{80} [%file : %line] %msg%n + + #下面的日志rolling策略和ROLLING_FILE_APPENDER的等价,保留最近30天的日志,每天的日志按照100MB分隔,日志总的大小上限为20GB + + mylog.log-%d{yyyy-MM-dd}.%i + 100MB + 30 + 20GB + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + +``` + +Logback的配置文件读取顺序(默认都是读取classpath下的):logback.groovy -> logback-test.xml -> logback.xml。如果想要自定义配置文件路径,那么只有通过修改logback.configurationFile的系统属性。 + +``` +System.setProperty("logback.configurationFile", "..."); +或者 +-Dlogback.configurationFile="xx" +``` + +Logback的日志级别:TRACE < DEBUG < INFO < WARN < ERROR。如果logger没有被分配级别,那么它将从有被分配级别的最近的祖先那里继承级别。root logger 默认级别是 DEBUG。 + +Logback中的logger同样也是有继承机制的。配置文件中的additivit也是为了不去继承rootLogger的配置,从而避免输出多份日志。 + +为了方便Log4j到Logback的迁移,官网提供了log4j.properties到logback.xml的转换工具:。 + +### 使用 + +Logback由于是天然与SLF4J集成的,因此它的使用也就是SLF4J的使用。 + +``` +import org.slf4j.LoggerFactory; + +private static final Logger LOGGER=LoggerFactory.getLogger(xx.class); + +LOGGER.info(" this is a test in {}", xx.class.getName()) +``` + +SLF4J同样支持占位符。 + +此外,如果想要打印json格式的日志(例如,对接日志到Logstash中),那么可以使用logstash-logback-encoder做为RollingFileAppender的encoder。 + +``` + +... + +``` + +### 性能优化 + +Logback提供了AsyncAppender进行异步日志输出,此异步appender实现上利用了队列做缓冲,使得日志输出性能得到提高。 + +``` + + ${root_log_dir}app.log + true + + %date [%level] [%thread] %logger{80} [%file : %line] %msg%n + + + ${root_log_dir}app.log.%d + + + + 0 + + 512 + + + + +``` + +这里需要特别注意以下两个参数的配置: + +- queueSize:队列的长度,该值会影响性能,需要合理配置。 +- discardingThreshold:日志丢弃的阈值,即达到队列长度的多少会丢弃TRACT、DEBUG、INFO级别的日志,默认是80%,设置为0表示不丢弃日志。 + +此外,由于是异步输出,为了保证日志一定会被输出以及后台线程能够被及时关闭,在应用退出时需要显示关闭logback。有两种方式: + +- 在程序退出的地方(ServletContextListener的contextDestroyed方法、Spring Bean的destroy方法)显式调用下面的代码。 + + ``` + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.stop(); + ``` + +- 在logback配置文件里,做如下配置。 + + ``` + + + + .... + + ``` + +## 3.3.5 日志门面 + +前面的四个框架是实际的日志框架。对于开发者而言,每种日志都有不同的写法。如果我们以实际的日志框架来进行编写,代码就限制死了,之后就很难再更换日志系统,很难做到无缝切换。 + +Java开发中经常提到面向接口编程,所以我们应该是按照一套统一的API来进行日志编程,实际的日志框架来实现这套API,这样的话,即使更换日志框架,也可以做到无缝切换。 + +这就是Commons-Logging与SLF4J这种日志门面框架的初衷。 + +### Apache Commons-Logging + +Apache Commons-Logging经常被简称为JCL,是Apache开源的日志门面框架。Spring中使用的日志框架就是JCL,使用起来非常简单。 + +``` +import org.apache.commons.logging.LogFactory; + +private static final Log LOGGER = LogFactory.getLog(xx.class); + +LOGGER.info("..."); +``` + +使用JCL需要先引入JCL的依赖: + +``` + + commons-logging + commons-logging + xx + +``` + +再来看一下如何让JCL使用其他日志实现框架: + +1. 这里当没有其他日志jar包存在的时候,JCL有自己的默认日志实现,默认的实现是对JUL的包装,即当没有其他任何日志包时,通过JCL调用的就是JUL做日志操作。 +2. 使用Log4j作为日志实现框架,那么只需要引入Log4j的jar包即可。 +3. 使用Log4j2作为日志实现,那么除了Log4j2的jar包,还需要引入Log4j2与Commons-Logging的集成包(使用SPI机制提供了自己的LogFactory实现): + + ``` + + org.apache.logging.log4j + log4j-jcl + xx + + ``` + +3. 使用Logback作为日志实现,那么由于Logback的调用是通过SLF4J的,因此需要引入jcl-over-slf4j包(直接覆盖了JCL的类),并同时引入SLF4J以及Logback的jar包。 + + ``` + + org.slf4j + jcl-over-slf4j + xx + + ``` + +### SLF4J + +SLF4J(Simple Logging Facade for Java)为Java提供的简单日志Facade。允许用户以自己的喜好,在工程中通过SLF4J接入不同的日志实现。与JCL不同的是,SLF4J只提供接口,没有任何实现(可以认为Logback是默认的实现)。 + +SLF4J的使用前提是引入SLF4J的jar包: + +``` + + + org.slf4j + slf4j-api + xx + +``` + +再看一下SLF4J如何和其他日志实现框架集成。 + +1. 使用JUL作为日志实现,需要引入slf4j-jdk14包。 + + ``` + + org.slf4j + slf4j-jdk14 + xx + + ``` + +1. 使用Log4j作为日志实现,需要引入slf4j-log4j12和log4j两个jar包。 + + ``` + + + org.slf4j + slf4j-log4j12 + xx + + + + + log4j + log4j + xx + + ``` + +1. 使用Log4j2作为日志实现,需要引入log4j-slf4j-impl依赖。 + + ``` + + + org.apache.logging.log4j + log4j-api + xx + + + org.apache.logging.log4j + log4j-core + xx/version> + + + + org.apache.logging.log4j + log4j-slf4j-impl + xx + + ``` + +1. 使用Logback作为日志实现,只需要引入logback包即可。 + +## 3.3.6 日志集成 + +上面说到了四种日志实现框架和两种日志门面框架。面对这么多的选择,即便是一个刚刚开始做的应用,也会由于依赖的第三方库使用的日志框架五花八门而造成日志配置和使用上的烦恼。得益于JCL和SLF4J,我们可以很容易的把日志都统一为一种实现,从而可以进行集中配置和使用。这里就以用Logback统一日志实现为例: + +1. 配置好Logback的依赖: + + ``` + + + org.slf4j + slf4j-api + xx + + + + ch.qos.logback + logback-core + xx + + + + ch.qos.logback + logback-classic + xx + + ``` + +1. 切换Log4j到SLF4J + + ``` + + org.slf4j + log4j-over-slf4j + xx + + ``` + +1. 切换JUL到SLF4J + + ``` + + org.slf4j + jul-to-slf4j + xx + + ``` + +1. 切换JCL到SLF4J + + ``` + + org.slf4j + jcl-over-slf4j + xx + + ``` + +这里需要注意的是,做了以上配置后,务必要排除其他日志包的存在,如Log4j。此外,在日常开发中经常由于各个依赖的库间接引入了其他日志库,造成日志框架的循环转换。比如同时引入了log4j-over-slf4j和slf4j-log4j12的情况,当使用SLF4J调用日志操作时就会形成循环调用。 + +笔者目前比较推崇的是使用SLF4J统一所有框架接口,然后都转换到Logback的底层实现。但这里需要说明的是Logback的作者是为了弥补Log4j的各种缺点而优化实现了SLF4J以及Logback,但不知为何作者又推出了Log4j2以期取代Log4j和Logback。所以,如果是一个新的项目,那么直接跳过Log4j和Logback选择Log4j2也是一个不错的选择, 官网也提供了Log4j到Log4j2的迁移说明。 + + diff --git a/book/chapter3-framework/media/framework.png b/book/chapter3-framework/media/framework.png new file mode 100644 index 0000000..6068904 Binary files /dev/null and b/book/chapter3-framework/media/framework.png differ diff --git a/book/chapter3-framework/media/spring-mvc.png b/book/chapter3-framework/media/spring-mvc.png new file mode 100644 index 0000000..3416b0c Binary files /dev/null and b/book/chapter3-framework/media/spring-mvc.png differ diff --git a/book/chapter3-framework/media/web-framework.jpg b/book/chapter3-framework/media/web-framework.jpg new file mode 100644 index 0000000..59ecd89 Binary files /dev/null and b/book/chapter3-framework/media/web-framework.jpg differ diff --git a/book/chapter3-framework/mvc.md b/book/chapter3-framework/mvc.md new file mode 100644 index 0000000..3427e2d --- /dev/null +++ b/book/chapter3-framework/mvc.md @@ -0,0 +1,595 @@ +# 3.4 Web MVC + +Web开发最终肯定是需要一个Web开发框架的,而MVC(Model View Controller)是Web开发中最常用的框架。从Struts1到Struts2,再到现在的Spring MVC、Jersy等等,都是MVC模式的实现框架。它将Web开发分为了模型、视图以及控制器三层,做到了职责分离,使得应用的模块能够高内聚、低耦合。 + +- 模型:代表业务数据和业务逻辑或者可以控制这些数据访问的模块 +- 视图:对模型的展现 +- 控制器:定义应用的各种行为 + +目前,互联网领域主要以Spring MVC为主要的Web开发框架,因此本节主要讲述Spring MVC的使用。Spring版本为4.3.7.RELEASE。 + +## 3.4.1 为什么是Spring MVC + +Spring MVC是Spring Web的一个重要模块。其结构简单,强大不失灵活,性能也很优秀。相比起其他的框架,具有但不限于以下特点: + +- 学习门槛低,易上手。 +- 由于Spring MVC框架封装的比较好,因此使用Spring MVC很容易写出优秀的程序。 +- Spring MVC继承了Spring框架的灵活性,非常易于扩展。 +- 框架的各个组件之间松耦合。 +- 支持多种视图展现。 +- 可以很方便的使用Spring生态下的组件。 + +## 3.4.2 Spring MVC处理流程 + +![](media/spring-mvc.png) + +如图所示是Spring MVC的几个关键组件。对于一个用户的Web请求的处理流程一般如下: + +1. 用户发起请求到DispatchServlet(在web.xml中配置, 是Spring mvc的前置控制器)。 +2. 从HandlerMapping中匹配此次请求信息的handler,匹配的条件包括:请求路径、请求方法、header信息等。常用的几个HandlerMapping有: + + - SimpleUrlHandlerMapping:简单的映射一个URL到一个Handler。 + - RequestMappingHandlerMapping: 扫描RequestMapping注解,根据相关配置,绑定URL到一个Handler。 + +3. 获取到对应的Handler(Controller,控制器)后, 调用相应的方法。这里牵扯到一个关键组件:HandlerAdapter,是Controller的适配器,Spring MVC最终是通过HandlerAdapter来调用实际的Controller方法的。常用的有以下几个: + + - SimpleControllerHandlerAdapter: 处理实现了Controller接口的Controller。 + - RequestMappingHandlerAdapter: 处理类型为HandlerMethod的handlder, 这里使用RequestMapping注解的Controller的方法就是一种HandlerMethod。 + +4. Handler执行完毕,返回相应的ModelAndView。 +5. 使用配置好的ViewResolver来解析返回结果。常用的几个ViewResolver: + + - UrlBasedViewResolver: 通过配置文件,根据URL把一个视图名交给到一个View来处理。 + - InternalResourceViewResolver类: 根据配置好的资源路径,解析为jsp视图,支持jstl。 + - FreeMarkerViewResolver:Freemarker的视图解析器。 + +6. 生成视图返回给用户。常用的几个的视图类如下: + + - MappingJackson2JsonView:使用mappingJackson输出JSON数据的视图,数据来源于ModelMap + - FreeMarkerView: 使用FreeMarker模板引擎的视图。 + - JSTLView: 输出jsp页面,可以使用jsp标准标签库。 + +此外,还有HandlerInterceptor和HandlerExceptionResolver两个重要组件。 + +- HandlerInterceptor是请求路径上的拦截器,需要自己实现这个接口,以拦截请求,做一些对Handler的前置和后置处理工作。 +- HandlerExceptionResolver是异常处理器。可以实现自己的处理器在全局层面拦截Handler抛出的Exception再做进一步的处理。Spring MVC自带了几个处理器: + + - SimpleMappingExceptionResolver: 可以将不同的异常映射到不同的jsp页面。 + - ExceptionHandlerExceptionResolver: 解析使用了@ExceptionHandler注解的方法来处理异常。 + - ResponseStatusExceptionResolver:处理@ResponseStatus注解的Exception。 + - DefaultHandlerExceptionResolver:默认的处理器,包括不支持method、不支持mediaType等等。 + +## 3.4.3 典型配置 + +要使用Spring MVC,首先就是需要配置DispatchServlet负责分发所有请求。 + +``` + + + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + + classpath:mvc-dispatcher-servlet.xml + + + 1 + + + mvc-dispatcher + / + +``` + +上面配置中,对应于servlet-mapping的url-pattern配置为/,是表示默认的URL映射,即当此次request匹配不到其他Servlet时,会默认进入此Servlet,包括静态资源请求。此外,对应于contextConfigLocation参数的mvc-dispatcher-servlet.xml则是对Spring MVC的一些配置。如果想要使用Spring的注解配置,则可以这么配置: + +``` + + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + contextConfigLocation + + me.rowkey.config.SpringWebConfig + + + 1 + +``` + +接着需要配置MVC相关的组件,如下: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + classpath:xx.properties + + + +``` + +1. mvc:default-servlet-handler是配置默认的Servlet作为静态资源的Handler。 +2. context:annotation-config是开启Spring的注解配置功能。 +3. mvc:annotation-driven是开启MVC的注解驱动,如创建了RequestMappingHandlerMapping和RequestMappingHandlerAdapter来处理注解handler。此外,上面配置了一些MessageConverter来处理各种使用了@ResposneBody标记返回数据的Handler。 +4. 配置了一个InternalResourceViewResolver处理返回数据。 +5. 配置exceptionResolver来处理异常。 +6. 配置CommonsMultipartResolver来处理文件上传。 +7. 配置PropertyPlaceholderConfigurer来读取相关资源文件,从而使得在配置文件中可以使用占位符填充,在代码中可以使用@Value注解来引用properties中的值。 + +此外,对应于MVC的namespace,还有以下几个常用配置: + +- mvc:interceptors: 配置拦截器 + + ``` + + + + + + + ``` + +- mvc:argument-resolvers: 配置自己实现的参数解析器,可用于参数的名称转换,如:API传递的参数是underScore时,可以统一转换为lowCamel。 + +这里需要注意的是,如果倾向于使用注解配置,那么对应于这些XML配置,Spring都提供了对应的注解,可以参考官方文档。一个简单的注解配置如下: + +``` +@EnableWebMvc +@Configuration +@ComponentScan("me.rowkey.pje.web") +public class SpringWebConfig { + +} + +@ControllerAdvice +public class ExceptionCatcher { + @ExceptionHandler + @ResponseBody + public String paramMissing(RuntimeException e) { + ... + } + +} +``` + +上面的@ControllerAdvice是Spring Web3.2后引入的注解。主要是为了统一对Controller添加@ExceptionHandler、@InitBinder、@ModelAttribute等注解,相比起之前写一个Base Controller做好相关配置然后每一个Controller去继承这种方式简化了很多。 + +## 3.4.4 零XML配置 + +自从Servlet3.0开始,可以完全脱离XML对Spring Web项目进行配置,如下所示: + +``` +public class MyWebApplicationInitializer implements + WebApplicationInitializer { + + @Override + public void onStartup(ServletContext appContext) + throws ServletException { + AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); + rootContext.register(SpringWebConfig.class); + + appContext.addListener(new ContextLoaderListener(rootContext)); + + ServletRegistration.Dynamic dispatcher = appContext.addServlet( + "dispatcher", new DispatcherServlet(rootContext)); + dispatcher.setLoadOnStartup(1); + dispatcher.addMapping("/"); + + } +} +``` + +以上就不需要再配置任何XML文件即可使得Spring MVC项目得以运行。原理如下: + +1. Servlet3.0加入了一个特性: 容器启动时会使用Java的SPI(Service Provider Interface)机制在启动的时候去读取META-INF/services下的javax.servlet.ServletContainerInitializer文件,并会对其中列出的每一个ServletContainerInitializer进行实例化并调用onStartup方法。 +2. Spring Web在自己的classpath:META-INF/services下的javax.servlet.ServletContainerInitializer文件中加入了org.springframework.web.SpringServletContainerInitializer一行,于是此类被实例化并调用。 +3. SpringServletContainerInitializer使用HandlesTypes声明自己处理实现了WebApplicationInitializer的类,于是上面我们新建的MyWebApplicationInitializer会被实例化并调用onStartup方法。 + +## 3.4.5 单元测试 + +Spring MVC支持对Controller的单元测试。 + +``` +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { + "classpath:mvc-dispatcher-servlet.xml", +}) +@WebAppConfiguration +public class ControllerJUnitBase { + + @Resource + private RequestMappingHandlerMapping handlerMapping; + + @Resource + private RequestMappingHandlerAdapter handlerAdapter; + + /** + * 执行request对象请求的action + * + * @param request + * @param response + * @return + * @throws Exception + */ + public ModelAndView excuteAction(HttpServletRequest request, HttpServletResponse response) throws Exception + + { + HandlerExecutionChain chain = handlerMapping.getHandler(request); + + final ModelAndView model = handlerAdapter.handle(request, response, chain.getHandler()); + return model; + } + + @Test + public void test throws Exception(){ + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/api/user/login"); + request.addParameter("mobile", "180xxxx3360"); + request.setMethod("POST"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + final ModelAndView mav = this.excuteAction(request, response); + Assert.assertEquals("user_login", mav.getViewName()); + } +} + +``` + +## 3.4.6 Web参数验证 + +Web开发中对前端传入的参数验证是一个关键的环节,对于每一个Controller都单独做验证是一种方式,但是更好的方式则是用一套框架将这个验证流程统一起来。 + +在Spring Web开发中,常用的验证方式主要有两种: + +- 支持Spring框架定义的Validator接口定义的校验。 +- 支持JSR303 Bean Validation定义的校验规范。 + +### Spring Validator + +此种方式是Spring框架自带的。 + +首先需要实现org.springframework.validation.Validator接口。 + +``` +pulic class User{ + private String name; + + ... +} + +public class UserValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return clazz.equals(User.class); + } + + @Override + public void validate(Object target, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "name", "user.name.required", "用户名不能为空"); + + User user = (User)target; + int length = user.getName().length(); + if(length > 10){ + errors.rejectValue("name", "user.name.too_long", "用户名不能超过{20}个字符"); + } + } +} +``` + +其次,需要设置Validator并触发校验。在Controller里增加方法并以@InitBinder注解,并在对应的Controller method中触发。 + +``` +@InitBinder +protected void initBinder(WebDataBinder binder){ + binder.setValidator(new UserValidator()); +} + +@RequestMapping (method = RequestMethod.POST) +public String reg(@Validated User user, BindingResult result){ + //校验没有通过 + if(result.hasErrors()){ + return "user"; + } + + if(user != null){ + userService.saveUser(user); + } + + return "user"; +} +``` + +如此,从页面提交的User对象可以通过我们实现的UserValidator类来校验,校验的结果信息存入BindingResult对象中。 + +### JSR303 Bean Validation + +在Spring3.1中增加了对JSR303 Bean Validation规范的支持,不仅可以对Spring的MVC进行校验,也可以对Hibernate的存储对象进行校验,是一个通用的校验框架。 + +这里必须要引入hibernate-validator,并开启MVC注解支持(), 它是JSR303规范的具体实现。 + +此外,需要对要校验的meta类的属性做注解Constraints限制。JSR303定义的Constraint如下: + +- @Null:验证对象是否为空 +- @NotNull:验证对象是否为非空 +- @AssertTrue:验证 Boolean 对象是否为 true +- @AssertFalse:验证 Boolean 对象是否为 false +- @Min:验证 Number 和 String 对象是否大等于指定的值 +- @Max:验证 Number 和 String 对象是否小等于指定的值 +- @DecimalMin:验证 Number 和 String 对象是否大等于指定的值,需要注意小数的精度问题 +- @DecimalMax: 验证 Number 和 String 对象是否小等于指定的值,需要注意小数的精度问题 +- @Size: 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 +- @Digits: 验证 Number 和 String 的构成是否合法 +- @Past: 验证 Date 和 Calendar 对象是否在当前时间之前 +- @Future: 验证 Date 和 Calendar 对象是否在当前时间之后 +- @Pattern: 验证 String 对象是否符合正则表达式的规则 + +此外,hibernate-validator也提供了一些注解支持,如: + +- @NotEmpty: 验证对象不为null也不为empty。 +- @NotBlank: 验证对象不为null也不为empty,连续的空格也认为是empty。 +- @Range:验证对象在指定的范围内。 + +配置很简单,只要对被校验的meta注解Constraint即可。 + +``` +pulic class User{ + @NotNull + private String name; + + ... +} +``` + +然后,在Controller的对应方法中,给对应的参数加@Valid注解。 + +``` +public String doRegister(@Valid User user, BindingResult result){ + + //校验没有通过 + if(result.hasErrors()){ + return "user"; + } + + if(user != null){ + userService.saveUser(user); + } + + return "user"; +} +``` + +这样就可以完成针对输入数据User对象的校验了,校验结果保存在BindingResult对象中。需要注意的一点是,BindingResult参数如果放在验证参数的后面,那么错误信息是会绑定到此BindingResult上的,否则会抛出MethodArgumentNotValidException异常。 + +## 3.4.7 异步Servlet + +Servlet3.0引入了异步Servlet,即Connector的线程只负责将请求派发到业务逻辑线程池即可,业务逻辑处理完成后再通过Servlet的异步上下文句柄将结果返回并响应, 能够避免Web Server的连接池被长期占用而引起性能问题。此外,异步Servlet还经常用在长轮训消息通知:客户端发起HTTP请求,设置一个较长的超时事件,服务端接受请求后如果有消息则直接返回,如果没有则hold住请求,一直等到有消息到达再发送响应。而客户端如果请求超时没有获取到消息,则继续循环/递归进行再一次的请求(超时时间可以自适应调整)。 + +Spring MVC提供了对异步Servlet的支持。 + +1. 在web.xml启用异步支持。 + + ``` + + Set Character Encoding + org.springframework.web.filter.CharacterEncodingFilter + true + ... + + + Set Character Encoding + /* + + + + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + ... + true + + ``` + + 这里需要注意除了Servlet之外,也要把所有经过的filter的async-supported都设置为true。 + +1. 实现异步Controller。 + + 返回结果为java.util.concurrent.Callable即为异步Contoller。 + + ``` + @Controller + @RequestMapping("/async") + public class CallableController { + @RequestMapping("/test") + @ResponseBody + public Callable callable() { + + return new Callable() { + @Override + public String call() throws Exception { + Thread.sleep(1000); + return "Asyn Controller Result"; + } + }; + } + } + ``` + +1. 配置异步Controller使用的线程池和超时参数。 + + ``` + + + + + + ``` + + 如此,处理业务的线程池即为myExecutor, 业务线程超时时间为2秒。 + +此外,如果想要针对具体的业务分别使用不同的线程池,那么可以通过返回org.springframework.web.context.request.async.DeferredResult进行。 + +``` +private Executor executor = Executors.newFixedThreadPool(200); //业务线程池 + +@RequestMapping("/deferred") +@ResponseBody +public DeferredResult quotes() { + DeferredResult deferredResult = new DeferredResult(2000L); //设置超时时间为2秒 + deferredResult.onCompletion(new Runnable() { + @Override + public void run() { + System.out.println("Deferred Result done !!!"); + } + }); + + executor.execute(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + deferredResult.setResult("Deferred Result"); + } + }); + + return deferredResult; +} +``` + +如此,请求将会被挂起,直到在其他线程中将DeferredResult放入数据才会响应, 并且能够给DeferredResult设置回调方法,在数据返回后做相应的后续操作。 + +还需要注意的是,当使用异步Servlet时,Tomcat等Servlet容器的线程池大小可以设置为1。 + +``` + +``` + +## 3.4.8 使用提示 + +1. 如果遇到一个项目中即提供了JSON API又提供了Web页面,那么单单配置一个ViewResolver是不行的。这时可以使用Spring MVC的视图协商器ContentNegotiatingViewResolver。配置如下: + + ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + data + status + desc + + + + + + + ``` + + 上面ContentNegotiationManager中配置的PathExtensionContentNegotiationStrategy表示根据path的扩展名来匹配返回的数据格式,如*.json就返回的JSON数据格式,此外,还配置了HeaderContentNegotiationStrategy根据header里的accept信息匹配返回数据格式,最后配置一个FixedContentNegotiationStrategy作为以上都无效时的默认返回数据格式。接着,ContentNegotiatingViewResolver又配置了一个解析为jsp页面的ViewResolver来处理返回格式为text/html的请求,如果此ViewResolver匹配不上,那么最后使用默认视图defaultViews来对ModelMap的值做JSON解析,上面的配置则是仅仅取ModelMap中对应于data、status以及desc的值作为JSON的视图字段。 + +1. 如果使用视图解析器解析handler的数据并返回响应视图,那么当Controller中的参数具有HttpServletResponse时,假若Controller没有返回值,那么此次请求是不会走到视图解析器的。如下: + + ``` + @RequestMapping(value = "test", method = RequestMethod.GET, headers = "Accept=text/html") + public void testApi(ModelMap modelMap, HttpServletRequest request, HttpServletResponse response, String id, int status){ + ...... + } + ``` + + Spring中对于这种含有HttpServletResponse参数的Controller认为是视图是由handler自己生成的。如果仍然想要走视图解析器,则必须要返回一个值。 + +1. API请求返回JSON/JSONP/XML数据的时候,可以通过@ResponseBody来注解Controller方法,并配置好对应的MessageConvertor。 +1. Spring的Controller、Service、DAO等都是单实例的,因此Controller、Service、DAO等各层组件,应该设计为有行为无状态、有方法无属性,即使有属性,也只是对下一层组件的持有。而与之对比,项目中的Entity、Domain、DTO等各种实体,有状态无行为,有属性无方法,即使有方法,也只是getter和setter等,围着状态打转。 +1. org/springframework/web/servlet路径下的DispatcherServlet.properties中配置了Spring MVC兜底使用的组件。即当项目的配置中缺少某一类组件的时候,Spring MVC会使用此文件中的相应组件来补充。 + diff --git a/book/chapter3-framework/orm.md b/book/chapter3-framework/orm.md new file mode 100644 index 0000000..f144586 --- /dev/null +++ b/book/chapter3-framework/orm.md @@ -0,0 +1,293 @@ +# 3.2 对象关系映射 + +ORM(Object Relational Mapping),对象关系映射,是一种为了解决面向对象与关系型数据库不匹配而出现的技术,使开发者能够用面向对象的方式使用关系型数据库。 + +目前最为常用的ORM框架主要是MyBatis(前身是ibatis)和Hibernate。两者对比如下: + +1. MyBatis非常简单易学,Hibernate相对较复杂,门槛较高。 +1. 相比起Hibernate,MyBatis灵活性更好。 +1. 系统数据处理量巨大,性能要求极为苛刻的情况下,MyBatis能够高度定制化SQL,因此会有更好的可控性和表现。 +1. MyBatis需要手写SQL语句,也可以生成一部分,Hibernate则基本上可以自动生成,偶尔会写一些HQL。同样的需求,MyBatis的工作量比Hibernate要大很多。类似的,如果涉及到数据库字段的修改,Hibernate修改的地方很少,而MyBatis要把那些SQL mapping的地方一一修改。 +1. 以数据库字段一一对应映射得到的PO和Hibernte这种对象化映射得到的PO是截然不同的,本质区别在于这种PO是扁平化的,不像Hibernate映射的PO是可以表达立体的对象继承,聚合等等关系的,这将会直接影响到你的整个软件系统的设计思路。 + +Hibernate现在主要用在传统企业应用的开发,互联网领域中由于流量大、并发高的缘故因此主要以MyBatis为主。笔者也推荐使用MyBatis做为ORM框架。 + +一般的ORM包括以下几个部分:    + +- 一个规定Mapping Metadata的工具, 即数据库中的表、列与对象以及对象属性的映射。    +- 一个对持久类对象进行CRUD操作的API。 +- 一个语言或API用来规定与类和类属性相关的查询。    +- 一种技术可以让ORM的实现同事务对象一起进行缓存、延迟加载等操作。 + +## 3.2.1 Mapping + +MyBatis支持XML配置: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +其中,environment元素体中包含了事务管理和连接池的配置,能够根据不同的环境使用不同的数据库配置。这里的dataSource设置为POOLED时使用了MyBatis自己提供的数据连接池。mappers元素则是包含一组mapper映射器(这些 mapper 的 XML 文件包含了 SQL 代码和映射定义信息)。 + +MyBatis也支持注解映射Mapper: + +``` +public interface TestUserMapper { + + @Select("SELECT * FROM test_user WHERE id = #{id}") + TestUser selectUser(int id); + +} +``` + +这里需要注意的是,命名空间现在是必需的。并且MyBatis对所有的命名配置元素的解析如果是全限定名则直接用;而如果是一个简单的名称,全局唯一的话没有问题,如果有重复类则会报错。 + +此外,MyBatis提供了mybatis-spring这个类库用于集成Spring和MyBatis。配置如下: + +``` + + + + + + + + ... + + + + + + + + + + + + + + + +``` + +## 3.2.2 CRUD以及属性的查询 + +MyBatis的核心是SqlSessionFactory,要使用它最基本的操作API,首先获取到SqlSessionFactory,然后再去获取相应的Mapper。 + +``` +String resource = "classpath:mybatis-config.xml"; +InputStream inputStream = Resources.getResourceAsStream(resource); +SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); + +SqlSession session = sqlSessionFactory.openSession(); +TestUserMapper mapper = session.getMapper(TestUserMapper.class); +TestUser testUser = mapper.selectUser(1); +... +``` + +此外,在Spring中可以使用MyBatis提供的org.mybatis.spring.SqlSessionTemplate进行操作。 + +``` +SqlSessionTemplate sqlTemplate = new SqlSessionTemplate(sqlSessionFactory); +TestUser testUser = (TestUser)this.sqlSessionTemplate.selectOne("me.rowkey.pje.mybatis.TestMapper.selecUser", uid); +``` + +这里需要注意的是,SqlSession是非线程安全的,而SqlSessionTemplate则是线程安全的。 + +MyBatis对应于CRUD有以下几个映射语句: + +- insert映射插入语句 +- update映射更新语句 +- delete映射删除语句 +- select映射查询语句 + +依赖于上述的四种操作,可以完成各种crud以及对属性之类的查询。MyBatis会自动把数据库查询结果注入到返回对象中。 + +这里需要注意的是上述的`select * from test_user where id = #{id}`,其中的#{id}是告诉MyBatis创建预处理语句属性并以它为背景设置安全的值, 对应于PreparedStatement中的?。此外,在MyBatis中还存在另一个符号$,如下: + +``` + +``` + +\$在这里的作用只是做字符串替换,不会修改或转义字符串。因此,能使用\#的地方就不要用$,除非是像order by这种不是参数的地方。 + +此外,MyBatis对于insert、update、delete以及select都提供了很多选项,用来支持注入缓存、自动映射、列名和属性名的转换等优化功能。 + +## 3.2.3 缓存 + +MyBatis支持一级缓存和二级缓存: + +- Mybatis默认开启一级缓存,是SqlSession级别的,Session结束那么缓存即清空。另外还支持Statement级别,即缓存只对当前的Statement有效(没什么用)。 +- MyBatis的二级缓存是Mapper级别的,被多个SqlSession共享,需要手动开启。 + +``` + + + + + + + +``` + +上述配置中的useCache设置为true即开启二级缓存,对应mapper中的所有select语句的结果集将会被缓存。也可以针对某个select设置useCache为false关闭二级缓存,设置flushCache为true使得数据直接flush到数据库中防止出现脏读数据。此外,二级缓存也可以配置成使用自定义的缓存实现。 + +``` + +``` + +需要注意的是MyBatis中的缓存设计初衷是针对单点应用的,都是本地缓存,现在的分布式应用慎重开启,只单纯把MyBastis作为一个ORM框架,在Service层自己实现缓存机制是更好的选择。 + +## 3.2.4 结果映射 + +MyBatis默认会自动映射查询结果,根据SQL返回的列名并在Java类中查找相同名字的属性(忽略大小写)。还可以全局配置对数据库列的LowUnderscore(a_column)命名转换为LowCamel命名(aColumn)。 + +``` + + + + + +``` + +除此之外,MyBatis还支持自定义映射。 + +``` +public class UserDto{ + private long uid; + private String userName; + + //getter and setters + .... +} + + + + + + + + + +``` + +上面使用的resultMap即做了自定义的映射工作,uid会自动映射, userName会使用自定义映射。 + +## 3.2.5 SQL语句构建器 + +虽然Mybatis已经做了很多封装,可以大大简化SQL编写的工作,但是很多时候在代码拼接SQL是无法避免的。如果用字符串自己进行拼接,那么各种+号、括号、引号、格式化问题会带来很多的麻烦,一不小心就会出错。针对这种状况,MyBatis提供了SQL语句构建类org.apache.ibatis.jdbc.SQL, 可以大大简化动态SQL编写的问题。 + +``` +new SQL() {{ + SELECT("user.id, user.user_name"); + SELECT("user.sign, user.gender"); + FROM("user_account user"); + INNER_JOIN("user_base_info uinfo on user.uid = uinfo.uid"); + WHERE("user.user_name like ?"); + OR(); + WHERE("uinfo.nick_name like ?"); + ORDER_BY("user.create_time"); + }}.toString(); +``` + +等同于 + +``` +"SELECT user.id, user.user_name, " +"user.sign, user.gender " + +"FROM user_account user " + +"INNER JOIN user_base_info uinfo on user.uid = uinfo.uid" + +"WHERE (user.user_name like ?) " + +"OR (uinfo.nick_name like ?) " + +"ORDER BY user.create_time"; +``` + +## 3.2.6 使用提示 + +1. 当几个SQL语句都包含同样的部分SQL逻辑时。可以使用来进行复用,如: + + ``` + + from test_meta where status = #{status} + + + + ``` + +2. 动态SQL中的空字符串判断 + + 在动态SQL中判断是否是空字符串时,MyBatis的内建机制不太好用。建议使用以下方式: + + ``` + + ``` + +3. 多ResultMap复用 + + 一个应用中由于功能的不同经常会有多个mapper.xml,如果一个文件需要另一个文件中的resultMap的定义,可以直接引用,而不需要再重新定义一遍。如: + + ``` + + + ... + + + + + + ... +