微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站。(关注公众号后回复”Java“即可领取 Java基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的Java学习指南、Java程序员面试指南等干货资源)
《码农翻身》读书笔记之Java帝国
Java帝国(Java语言发展,JVM和class,持久化和IO,JDBC与DP,JTA与分布式事务,JSP与servlet等等内容)
这是我的后端读书笔记系列文章的第二篇,选取的是最近刚刚圈粉的知名博主刘欣创作的《码农翻身》。这篇文章只是第一部分。
本文内容主要根据知名博主刘欣一作《码农翻身》的内容总结而来,本书的内容风趣幽默,讲解计算机理论原理也是十分透彻,由于书中常常以小故事的形式出现,为了方便学习和回顾,我把它们进行了一些改编和整理,便于自己和跟多人阅读。
本篇文章主要讲述的书中的第二章“Java帝国”。
本文首发于我的个人博客:https://h2pl.github.io/
同时发表在csdn技术博客上:https://blog.csdn.net/a724888
也欢迎来我的GitHub中交流学习:https://github.com/h2pl/
Java帝国的诞生
c语言诞生:
1995年时,c语言已经诞生了20年,统治了编程界,它贴近硬件运行极快,效率高,Linux也是使用c语言编写的。
程序员们用c开发了很多系统级软件,操作系统,数据库,编译期等等,正是因为它与底层接近,提供了内存管理和指针的特性,让这些程序的运行效率很高。
c语言弊病:
但是c语言的内存管理和指针确是程序员的枷锁,程序员必须手动释放内存,并且操作指针时不会有越界提醒,于是运行时会暴露很多问题,程序员需要花大量时间处理指针和内存分配。
C语言由于贴近底层,在跨平台方面很差劲,容易产生不兼容的情况,而且必须使用c++标准库,否则容易出问题。c++在c语言的基础上出现,提供了面向对象的特性,让企业级开发得心应手。但是他的特性非常复杂,学习难度很大。
Java诞生:
于是Java出现了。他支持跨平台的运行,不用考虑内存管理和指针,也支持面向对象。Java程序必须运行在虚拟机上,这个虚拟机提供了一层抽象,它本身也是c语言编写,用于真正和操作系统交互,但是对于程序员来说是透明的,更加友好和易于掌握。
Java发展:
Java帝国推出了J2SE,J2ME,J2EE三个类别的应用,J2SE主打applet,由于要求浏览器运行jvm,所以被早早淘汰,J2ME没有等到移动互联网到来就草草收场。
J2EE支持的是企业级开发,运行在服务器上,支持很多大型项目需要的特性,于是weblogic和websphere等服务器借着J2EE大热了一把。
帝国版图:
构建构建:maven和ant
应用服务器:Tomcat,jetty,jboss等
开发工具。。等等。
.NET,ruby和php等语言都难以撼动Java的地位。
Android出现后也改变了移动端。
Hadoop等大数据技术也采用Java。
我是一个Java class
类加载
1 Java class是一个字节码文件,本质上是0101的字节流。
JVM会使用类加载器加载这些class文件,并且进行验证(按照class格式检查),解析,加载,初始化等过程,然后在堆中生成一个对应的Class对象。
2 加载一个类时,需要遵循双亲委派模型,先让父类加载器检查是否能加载该类,如果可以的话,就由父类加载器进行加载,否则依次往下询问即可。
因为有个黑客写过一个String类植入漏洞代码,让自定义加载器加载他,导致问题。所以我们需要双亲委派加载模型。
当然父类加载器的权利比较大,一般加载的是lib下面的类,扩展加载器则加载lib\ext下面的类,其余的类就由appclassloader根据classpath(自定义类输出目录)目录来加载了。
jvm内存模型和GC
1 JVM把内存分成多个逻辑区域,方法区用来保存类的元数据,堆区中保存类的对象。每当线程操作方法时要先新建一个栈帧压入虚拟机栈(每个线程独有的栈)中。栈帧里又会有局部变量表和操作数栈。
2 GC垃圾回收负责回收无用的对象,通过可达性分析标记和GC Root相连的对象,清理其他对象。
3 实际上Java代码是保存在项目目录下的,而编译时会把Java代码编译成class文件并放到classpath目录下,然后才能执行,编译一般通过javac命令完成,执行class文件或者jar包一般需要指定入口方法,比如main函数。
Java的持久化大法
1 Java运行在内存中,断电时自然丢失数据结束运行,所以必须要有一些持久化方案来保存数据。Java IO可以把对象转储成二进制文件,放到硬盘中,也就是序列化,然后需要的时候再反序列化还原即可。
2 使用数据库进行持久化:
数据库可以转储数据,并且把Java中的对象按照表格数据的形式进行转储,但是手动地进行转换操作显然需要大量代码。于是后来出现了ORM框架,简化了这一映射过程。
要让Java使用数据库,那么就必须先和数据库通信,当然也需要基于socket开发,和数据库协商协议,然后建立TCP连接,最后完成通信。JDBC完成了这个使命。
ORM框架
1 J2EE拿出了EJB作为企业级的服务器中间件,并且把安全,事务,分布式,以及ORM的特性都糅合进去。但是却十分低效。
orm框架hibernate出现了,专注于对象到数据库的映射。而ibatis出现以后,更是可以方便地采用sql和映射两种方式完成orm。
2 Spring出现以后逐渐取代EJB提供了更加轻量级的企业级开发特性,并且使用jdbctemplate集成了hibernate和ibatis等持久化框架。
JDBC的诞生
早期connection
MySQL其实已经定义好了应用层协议,一般都会开启3306端口进行监听,以便接收TCP请求。
然而,如果直接基于socket进行通信,显然是不合适的,所以JDBC抽象出一个叫做connection的类,来表示一个MySQL的数据库连接。同时,使用statement表示sql的执行实体类,又用resultset表示查询结果集。
由于Connection应该由不同的数据库分别提供,所以connection只是接口,实现类要由mysql等数据库提供jar包。
connection的封装之路
写死实现类:
由于数据库提供了connection实现类,我们在代码里也可以写死实现类。但是一旦jar包更新,它的类名改变,那我们的代码编译也会出错。这样看来我们不应该直接进行实例化。
简单工厂:
不直接实例化,那么可以考虑使用简单工厂,我们把这个工厂叫做Driver,让工厂给我们一个connection实例,根据传入参数工厂会返回给我们对应的实例。
基于配置文件进行反射的简单工厂:
使用上述方案显然会有问题,当我们需要增加其他数据库的支持时,就必须要更改代码。为了解决这个问题,我们改变之前向工厂传递参数的策略,改用配置文件的方式,把connection实现类的全限定名配置到配置文件中。
由于有了类的全限定名,工厂中只需要进行反射调用完成实例化,即使有新的类要支持,也只需修改配置文件即可。
工厂方法实现:
上述方法虽然可行,但是用户在使用时必须要提供一个配置文件,并且配置文件中要有类的全限定名,这很低级。还有一点就是创建实现类的过程不应该被暴露出来,用户只需要得到connection实例即可。
于是我们可以用工厂方法来实现,每个数据库提供一个Driver实现类来实现Driver接口需要提供getconnection方法,封装实现细节。用户首先加载Driver实现类,再通过反射获取driver实例,从而获取connection。这样子避免使用配置文件并且封装细节。
drivermanager静态方法实现:
其实上面这个方案基本ok了。只不过要求用户必须懂反射。更加人性化的方式是使用一个drivermanager类来管理所有的Driver实现类,通过一个集合来管理已注册的Driver实现类,通过输入参数来获取指定Driver工厂生产的connection实例。
用户需访问drivermanager的静态方法getconnection就可以获取connection实例了。用户先使用class.forname加载对应的driver,再通过上述方法获取connection实例即可,不依赖于具体实现类,主要导入包,执行方法即可,是最优雅的实现。
Java帝国之宫廷内斗
本地事务:
DBC支持事务,默认的实现就自动开启事务,执行提交操作时才会提交事务。但是事实上,JDBC支持的事务是本地事务,当遇到多个数据库的问题时,就可能产生分布式事务的问题。
比如说A转账给B,A和B的数据分别在A库和B库,于是对A扣款和对B加款两个操作分别是两个事务,如何把保证两个事务合为一个事务的原子性,就需要使用分布式事务。
全局事务管理器和分布式事务:
2PC是一种分布式事务的解决方案,首先我们设置一个全局事务管理器来协调各个数据库的事务提交,全局事务管理向各个数据库发出准备消息,各个数据库准备好,锁资源,维护日志,执行操作但是不提交。
当全局事务管理器得到所有数据库执行成功的消息之后才会通知每个事务提交,如有一个失败则通知回滚。
显然全局事务管理器要负责和每个数据库通信。但事实上,2PC存在两个问题,一是全局事务管理器的宕机问题,另一个是数据库的宕机问题。
JTA的事务实现
基于全局事务管理的JTA实现只需要打开一个全局事务,然后执行每个数据库的操作,最后进行提交,如果抛出异常时则进行回滚,用起来也确实很方便。
但是全局事务的实现虽然保证了强一致性,但是性能不好,因为经常需要锁资源,并且需要消耗多节点的通信时间。
最终一致性方案:消息队列实现
事实上不需要使用2PC的分布式事务也可以保证数据库事务的最终一致性。
可以通过消息队列+事务的方式实现,比如A转账给B,首先执行A的转账事务,我们在A库中维护一个消息表记录转账消息,然后转账时往这个表中写入一条消息。由于是同一个表,所以这两个操作可以写在同一个事务里,保证了原子性。
然后我们开启线程轮询消息表进行消息发送,发送给执行B加款服务的服务器上,对方服务器收到消息后,根据信息执行对应的事务方法。
为了实现幂等性的接口,防止消息重复,B所在的服务也要在B
库中维护一张消息表,记录已经接收的消息,避免重发。
事务消息
实际上还可以通过支持事务消息的消息队列来实现分布式事务。
这样的消息队列不需要我们自己维护消息表和发送服务,只需要使用消息队列提供的事务消息机制就可以完成事务操作。
装配工JSP的没落
1 最早以前的动态web应用需要使用c语言开发cgi程序,把一段段html代码通过c的输出流打印出来,非常低级。
2 后来asp出现了,静态页面里可以嵌入动态代码。随后jsp也出现了,和asp类似,在html插入Java代码。
3 jsp支持使用jstl模板语言,更方便嵌入动态改变的代码,每次渲染时都会解析成Java语言,最后实例化成一个servlet以提供服务。
4 由于jsp经常嵌入大量Java代码,是在太不优雅了,于是出现了一些模板引擎,用它们来装配html代码。这些模板引擎的语法和jsp类似,只不过jsp只能跑在web容器里,而模板引擎却可脱离web环境。
5 再后来,前端语言js兴起,前端不需要让后端来组装html页面了,前端自己可以提供引擎进行模板组装,后端只需要提供数据接口和模板就可以了,前端js拿到模板和json数据后就可以自己完成装配。模板语言和jsp这类装配工没有了往日的辉煌。
Java帝国之消息队列
1 从单机部署到多机部署,本地调用也变成了远程调用
可以使用消息队列实现远程调用,异步处理消息即可。消息队列会保证消息有序,可靠,以及持久化。
2 很多消息队列面世,但是这些消息队列都没有一个固定的实现方式,而是大家各自为政,互不兼容。Java为了统一这些实现,设计了JMS。
3 一般的消息队列至少要分为几个步骤,首先是绑定一个主机,该主机提供消息服务,然后建立连接,随后在连接上定义一个队列,并且把消息发到这个队列里。
rabbitmq对这些步骤做了一部分封装。
1 首先通过连接工厂获取一个连接实例。
2 在这个连接上打开一个channel信道,类似于socketchannel。
3 在channel上声明一个队列queue。
4 往queue中发送信息
统一抽象
我们可以抽象出几个部分,首先是连接接口connection,这个接口可以由不同MQ提供实现。
然后是Session,connection可以理解为tcp连接,session则是应用层的会话,要操作队列,就要打开session。
接着就是生产者,消费者,以及队列本身。
其实这三个组件都是通过session进行建立和绑定的。
配置合代码分离。
connectionfactory工厂不应该直接实例化,而是通过外部注入。
为了构造不同的connection对象,我们需要注入配置文件,考虑到不同厂商提供的connectionfactory实现不同,所以可以使用JNDI方式获取factory工厂,jndi服务配置在web容器,暴露服务名即可。
JMS如何支持发布订阅模型
删除抽象支持的是点对点的消息模型,但事实上很多mq支持发布订阅的模型,一条消息可被多个生产者消费。
为了实现这个模式,我们可以将原来的queue和topic一起,抽象为destination,而不只是一个队列。
destination如何处理消息可以由具体的实现类来决定,生产者和消费者可以由mq实现决定如何消费destination里的消息。
这就是JMS的实现方式了。
JMS实际流程
1 首先通过jndi获取connectionfactory
2 通过jndi获取消息队列的实例。
3 建立连接,然后创建session。
4 通过session创建生产者发送消息。
5 通过session创建消费者读取队列消息
6 往队里发消息和取消息。
Java帝国之动态代理
Java运行期不允许动态修改类,所有很多动态性没办法实现,而像Python,php等却可以。
由于不能动态改变类,如果我们要加入一些切面逻辑,比如事务,日志以及权限管理等功能,就必须修改原来的代码,但是显然是很复杂和低效的。
动态代理解决
使用动态代理,虽然不能动态修改类,却可以动态生成新类并完成加载,然后然新对象执行切面方法,再执行原来的逻辑。
实现了面向切面编程。
比如事务的实现,只需要在代理处理器中加入事务管理器的提交和回滚代码即可。
JDK动态代理要求必代理类和被代理类都要实现同一个接口。而cglib则不用。
Java注解是怎么成功上位的
以前大家都用xml作为唯一指定配置文件。
但是后来Java注解出现了,由于jdk允许自定义注解,并且程序可以通过反射获取注解,于是注解的简单易用性赢得了很多开发者的青睐,逐渐地取代了xml。
注解的使用需要定义一个@interface接口,注明target和retention。
xml配置臃肿,注解轻便快捷。
但实际上,Java注解比较分散不利于集中管理,一般用于像mvc的注解,而xml可以用在综合的配置文件里,比如maven,spring配置种。
使用json可以代替xml臃肿的配置。
Java帝国之泛型
Java最早不支持泛型,但是c++支持泛型,Java借鉴这一特性,加入了伪泛型机制。
使用泛型擦除来实现,用T来表示传入类型,比如List,然后传入一个类型的对象后不能再传入其他类型了。编译时会进行泛型擦除变成object类型,使用时会强制转成传入类型。
1 泛型继承
2 通配符
日志系统的诞生
最早的Java打印日志就是靠控制台简陋的输出。
为了更好地管理和打印日志
log4j
实际上可以把日志系统分为几个部分,一个是输出控制器,我们叫appender,一个是格式化类,用于格式化日志。还有一个是日志对应的包或者类,可以作为一个日志输出的条件。
最后还需要一个优先级来表示日志的级别。
1 appender可以有文件实现,控制台实现,以及其他方式
2 format可以是普通格式,也可以是xml,或者是html。
3 logger作为一个类可以传入包路径或者类名,以便指定日志处理范围。
4 优先级则会用5个常量表示,debug,info,warn,error,fatal。从低级到高级排列。
这个设计其实就是log4j的设计。
slf4j。
越来越多日志系统出现,于是设计了一个抽象层,slf4j,提供日志管理的api,并且可以调整底层实际的日志系统,比如log4j,logback,jdk的logging等实现。
序列化的咸鱼翻身
1 要把Java对象保存到硬盘中,必须进行序列化,变成二进制数据,当然,要在网络中传输,实际上也是二进制数据。
只不过应用层的协议不一定是使用二进制进行数据序列化,还可以用xml和json.
2 使用xml的多余数据很多,消耗的空间也比较大,而二进制数据则更加轻量级。
但是xml可以支持多语言,按照特定协议的描述格式进行传输,,就可以转化成对应语言的对象。json可以更加精简地传输数据,逐渐占领了xml的市场。
3 而Java的序列化支持Java对象的序列化和反序列化,不支持多语言。Java为了实现多语言支持,决定定义一种消息格式,来支持不同语言的序列化与反序列化。
Java中的锁
Java中的锁主要就是互斥锁。乐观锁可以使用cas实现。
Spring的本质
aop就是动态代理
ioc则是控制反转,依赖注入,使用反射生成实例,通过配置文件和注解等方式指定实现类。用户代码依赖接口而不依赖与实现。