(笔记整合)Java基础四

三十一、Java应用开发中的注入攻击

典型回答
注入式(Inject)攻击是一类非常常见的攻击方式,其基本特征是程序允许攻击者将不可信的动态内容注入到程序中,并将其执行,这就可能完全改变最初预计的执行过程,产生恶意效果。

下面是几种主要的注入式攻击途径,原则上提供动态执行能力的语言特性,都需要提防发生注入攻击的可能。

  • 最常见的SQL注入攻击。一个典型的场景就是Web系统的用户登录功能,根据用户输入的用户名和密码,我们需要去后端数据库核实信息。
  • 操作系统命令注入。Java语言提供了类似Runtime.exec(…)的API,可以用来执行特定命令,假设我们构建了一个应用,以输入文本作为参数。
  • XML注入攻击。Java核心类库提供了全面的XML处理、转换等各种API,而XML自身是可以包含动态内容的,例如XPATH,如果使用不当,可能导致访问恶意内容。

注入式攻击,可以有不同角度、不同层面的解决方法,例如针对SQL注入:

  • 在数据输入阶段,填补期望输入和可能输入之间的鸿沟。可以进行输入校验,限定什么类型的输入是合法的,例如,不允许输入标点符号等特殊字符,或者特定结构的输入。
  • 在Java应用进行数据库访问时,如果不用完全动态的SQL,而是利用PreparedStatement,可以有效防范SQL注入。不管是SQL注入,还是OS命令注入,程序利用字符串拼接生成运行逻辑都是个可能的风险点!
  • 在数据库层面,如果对查询、修改等权限进行了合理限制,就可以在一定程度上避免被注入删除等高破坏性的代码。

在安全领域,有一句准则:安全倾向于 “明显没有漏洞”,而不是“没有明显漏洞”。所以,为了更加安全可靠的服务,我们最好是采取整体性的安全设计和综合性的防范手段,而不是头痛医头、脚痛医脚的修修补补,更不能心存侥幸。

三十二、如何写出安全的Java代码?

典型回答
这个问题可能有点宽泛,我们可以用特定类型的安全风险为例,如拒绝服务(DoS)攻击,分析Java开发者需要重点考虑的点。

DoS是一种常见的网络攻击,有人也称其为“洪水攻击”。最常见的表现是,利用大量机器发送请求,将目标网站的带宽或者其他资源耗尽,导致其无法响应正常用户的请求。

从Java语言的角度,更加需要重视的是程序级别的攻击,也就是利用Java、JVM或应用程序的瑕疵,进行低成本的DoS攻击,这也是想要写出安全的Java代码所必须考虑的。例如:

  • 如果使用的是早期的JDK和Applet等技术,攻击者构建合法但恶劣的程序就相对容易,例如,将其线程优先级设置为最高,做一些看起来无害但空耗资源的事情。幸运的是类似技术已经逐步退出历史舞台,在JDK 9以后,相关模块就已经被移除。
  • 哈希碰撞攻击,就是个典型的例子,对方可以轻易消耗系统有限的CPU和线程资源。从这个角度思考,类似加密、解密、图形处理等计算密集型任务,都要防范被恶意滥用,以免攻击者通过直接调用或者间接触发方式,消耗系统资源。
  • 利用Java构建类似上传文件或者其他接受输入的服务,需要对消耗系统内存或存储的上限有所控制,因为我们不能将系统安全依赖于用户的合理使用。其中特别注意的是涉及解压缩功能时,就需要防范Zip bomb等特定攻击。
  • 另外,Java程序中需要明确释放的资源有很多种,比如文件描述符、数据库连接,甚至是再入锁,任何情况下都应该保证资源释放成功,否则即使平时能够正常运行,也可能被攻击者利用而耗尽某类资源,这也算是可能的DoS攻击来源。

针对序列化,通常建议:

  • 敏感信息不要被序列化!在编码中,建议使用transient关键字将其保护起来。
  • 反序列化中,建议在readObject中实现与对象构件过程相同的安全检查和数据检查。

三十三、后台服务出现明显“变慢”,谈谈诊断思路?

典型回答
需要对这个问题进行更加清晰的定义:

  • 服务是突然变慢还是长时间运行后观察到变慢?类似问题是否重复出现?
  • “慢”的定义是什么,我能够理解是系统对其他方面的请求的反应延时变长吗?

理清问题的症状,这更便于定位具体的原因,有以下一些思路:

  • 问题可能来自于Java服务自身,也可能仅仅是受系统里其他服务的影响。初始判断可以先确认是否出现了意外的程序错误,例如检查应用本身的错误日志。
    对于分布式系统,很多公司都会实现更加系统的日志、性能等监控系统。一些Java诊断工具也可以用于这个诊断,例如通过JFR(Java Flight Recorder),监控应用是否大量出现了某种类型的异常。
    如果有,那么异常可能就是个突破点。
    如果没有,可以先检查系统级别的资源等情况,监控CPU、内存等资源是否被其他进程大量占用,并且这种占用是否不符合系统正常运行状况。
  • 监控Java服务自身,例如GC日志里面是否观察到Full GC等恶劣情况出现,或者是否Minor GC在变长等;利用jstat等工具,获取内存使用的统计信息也是个常用手段;利用jstack等工具检查是否出现死锁等。
  • 如果还不能确定具体问题,对应用进行Profling也是个办法,但因为它会对系统产生侵入性,如果不是非常必要,大多数情况下并不建议在生产系统进行。
  • 定位了程序错误或者JVM配置的问题后,就可以采取相应的补救措施,然后验证是否解决,否则还需要重复上面部分过程。

三十四、“Lambda能让Java程序慢30倍”,你怎么看?

典型回答
“Lambda能让Java程序慢30倍”这个争论实际反映了几个方面:
第一,基准测试是一个非常有效的通用手段,让我们以直观、量化的方式,判断程序在特定条件下的性能表现。

第二,基准测试必须明确定义自身的范围和目标,否则很有可能产生误导的结果。前面代码片段本身的逻辑就有瑕疵,更多的开销是源于自动装箱、拆箱(autoboxing/unboxing),而不是源自Lambda和Stream,所以得出的初始结论是没有说服力的。

第三,虽然Lambda/Stream为Java提供了强大的函数式编程能力,但是也需要正视其局限性:

  • 一般来说,我们可以认为Lambda/Stream提供了与传统方式接近对等的性能,但是如果对于性能非常敏感,就不能完全忽视它在特定场景的性能差异了,例如:初始化的开销。Lambda并不算是语法糖,而是一种新的工作机制,在首次调用时,JVM需要为其构建CallSite实例。这意味着,如果Java应用启动过程引入了很多Lambda语句,会导致启动过程变慢。其实现特点决定了JVM对它的优化可能与传统方式存在差异。
  • 增加了程序诊断等方面的复杂性,程序栈要复杂很多,Fluent风格本身也不算是对于调试非常友好的结构,并且在可检查异常的处理方面也存在着局限性等。

三十五、JVM优化Java代码时都做了什么?

JVM在对代码执行的优化可分为运行时(runtime)优化和即时编译器(JIT)优化。运行时优化主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配机制(如TLAB)等。除此之外,还有一些专门用于优化解释执行效率的,比如说模版解释器、内联缓存(inline cache,用于优化虚方法调用的动态绑定)。

JVM的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。它采用了多种优化方式,包括静态编译器可以使用的如方法内联、逃逸分析,也包括基于程序运行profle的投机性优化(speculative/optimistic optimization)。这个怎么理解呢?比如我有一条instanceof指令,在编译之前的执行过程中,测试对象的类一直是同一个,那么即时编译器可以假设编译之后的执行过程中还会是这一个类,并且根据这个类直接返回instanceof的结果。如果出现了其他类,那么就抛弃这段编译后的机器码,并且切换回解释执行。

当然,JVM的优化方式仅仅作用在运行应用代码的时候。如果应用代码本身阻塞了,比如说并发时等待另一线程的结果,这就不在JVM的优化范畴了。

Java代码的整个生命周期
在这里插入图片描述

三十六、MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?

典型回答
所谓隔离级别(Isolation Level),就是在数据库事务中,为保证并发数据读写的正确性而提出的定义,它并不是MySQL专有的概念,而是源于ANSI/ISO制定的SQL-92标准。

每种关系型数据库都提供了各自特色的隔离级别实现,虽然在通常的定义中是以锁为实现单元,但实际的实现千差万别。以最常见的MySQL InnoDB引擎为例,它是基于MVCC(Multi-Versioning Concurrency Control)和锁的复合实现,按照隔离程度从低到高,MySQL事务隔离级别分为四个不同层次:

  • 读未提交(Read uncommitted),就是一个事务能够看到其他事务尚未提交的修改,这是最低的隔离水平,允许脏读出现。
  • 读已提交(Read committed),事务能够看到的数据都是其他事务已经提交的修改,也就是保证不会看到任何中间性状态,当然脏读也不会出现。读已提交仍然是比较低级别的隔离,并不保证再次读取时能够获取同样的数据,也就是允许其他事务并发修改数据,允许不可重复读和幻象读(Phantom Read)出现。
  • 可重复读(Repeatable reads),保证同一个事务中多次读取的数据是一致的,这是MySQL InnoDB引擎的默认隔离级别,但是和一些其他数据库实现不同的是,可以简单认为MySQL在可重复读级别不会出现幻象读。
  • 串行化(Serializable),并发事务之间是串行化的,通常意味着读取需要获取共享读锁,更新需要获取排他写锁,如果SQL使用WHERE语句,还会获取区间锁(MySQL以GAP锁形式实现,可重复读级别中默认也会使用),这是最高的隔离级别。

至于悲观锁和乐观锁,也并不是MySQL或者数据库中独有的概念,而是并发编程的基本概念。主要区别在于,操作共享数据时,“悲观锁”即认为数据出现冲突的可能性更大,而“乐观锁”则是认为大部分情况不会出现冲突,进而决定是否采取排他性措施。

反映到MySQL数据库应用开发中,悲观锁一般就是利用类似SELECT … FOR UPDATE这样的语句,对数据加锁,避免其他事务意外修改数据。乐观锁则与Java并发包中的AtomicFieldUpdater类似,也是利用CAS机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。

前面提到的MVCC,其本质就可以看作是种乐观锁机制,而排他性的读写锁、双阶段锁等则是悲观锁的实现。

对数据库相关领域学习,从最广泛的应用开发者角度,至少需要掌握:

  • 数据库设计基础,包括数据库设计中的几个基本范式,各种数据库的基础概念,例如表、视图、索引、外键、序列号生成器等,清楚如何将现实中业务实体和其依赖关系映射到数据库结构中,掌握典型实体数据应该使用什么样的数据库数据类型等。
  • 每种数据库的设计和实现多少会存在差异,所以至少要精通你使用过的数据库的设计要点。我今天开篇谈到的MySQL事务隔离级别,就区别于其他数据库,进一步了解MVCC、Locking等机制对于处理进阶问题非常有帮助;还需要了解,不同索引类型的使用,甚至是底层数据结构和算法等。
  • 常见的SQL语句,掌握基础的SQL调优技巧,至少要了解基本思路是怎样的,例如SQL怎样写才能更好利用索引、知道如何分析SQL执行计划等。
  • 更进一步,至少需要了解针对高并发等特定场景中的解决方案,例如读写分离、分库分表,或者如何利用缓存机制等,目前的数据存储也远不止传统的关系型数据库了。

目前最为通用的Java和数据库交互技术就是JDBC,最常见的开源框架基本都是构建在JDBC之上,包括我们熟悉的JPA/Hibernate、MyBatis、Spring JDBC Template等,各自都有独特的设计特点。

  • Hibernate是最负盛名的O/R Mapping框架之一,它也是一个JPA Provider。顾名思义,它是以对象为中心的,其强项更体现在数据库到Java对象的映射,可以很方便地在Java对象层面体现外键约束等相对复杂的关系,提供了强大的持久化功能。内部大量使用了Lazy-load等技术提高效率。并且,为了屏蔽数据库的差异,降低维护开销,Hibernate提供了类SQL的HQL,可以自动生成某种数据库特定的SQL语句。
  • Hibernate应用非常广泛,但是过度强调持久化和隔离数据库底层细节,也导致了很多弊端,例如HQL需要额外的学习,未必比深入学习SQL语言更高效;减弱程序员对SQL的直接控制,还可能导致其他代价,本来一句SQL的事情,可能被Hibernate生成几条,隐藏的内部细节也阻碍了进一步的优化。
  • 而MyBatis虽然仍然提供了一些映射的功能,但更加以SQL为中心,开发者可以侧重于SQL和存储过程,非常简单、直接。如果我们的应用需要大量高性能的或者复杂的SELECT语句等,“半自动”的MyBatis就会比Hibernate更加实用。
  • 而Spring JDBC Template也是更加接近于SQL层面,Spring本身也可以集成Hibernate等O/R Mapping框架。

从架构设计的角度,可以将MyBatis分为哪几层?每层都有哪些主要模块?

  • 基础支撑层,主要是用来做连接管理、事务管理、配置加载、缓存管理等最基础组件,为上层提供最基础的支撑。
  • 数据处理层,主要是用来做参数映射、sql解析、sql执行、结果映射等处理,可以理解为请求到达,完成一次数据库操作的流程。
  • API接口层,主要对外提供API,提供诸如数据的增删改查、获取配置等接口。

三十七、Spring Bean的生命周期和作用域?

典型回答
创建Bean会经过一系列的步骤,主要包括:

  • 实例化Bean对象。
  • 设置Bean属性。
  • 如果我们通过各种Aware接口声明了依赖关系,则会注入Bean对容器基础设施层面的依赖。具体包括BeanNameAware、BeanFactoryAware和ApplicationContextAware,
  • 分别会注入Bean ID、Bean Factory或者ApplicationContext。
  • 调用BeanPostProcessor的前置初始化方法postProcessBeforeInitialization。
  • 如果实现了InitializingBean接口,则会调用afterPropertiesSet方法。
  • 调用Bean自身定义的init方法。
  • 调用BeanPostProcessor的后置初始化方法postProcessAfterInitialization。
  • 创建过程完毕。
    在这里插入图片描述
    Spring Bean的销毁过程会依次调用DisposableBean的destroy方法和Bean自身定制的destroy方法。

Spring Bean有五个作用域:

  • Singleton,这是Spring的默认作用域,也就是为每个IOC容器创建唯一的一个Bean实例。
  • Prototype,针对每个getBean请求,容器都会单独创建一个Bean实例。
  • Request,为每个HTTP请求创建单独的Bean实例。
  • Session,很显然Bean实例的作用域是Session范围。
  • GlobalSession,用于Portlet容器,因为每个Portlet有单独的Session,GlobalSession提供一个全局性的HTTP Session。

从Bean的特点来看,Prototype适合有状态的Bean,而Singleton则更适合无状态的情况。另外,使用Prototype作用域需要经过仔细思考,毕竟频繁创建和销毁Bean是有明显开销的。

Spring AOP引入了其他几个关键概念:

  • Aspect,通常叫作方面,它是跨不同Java类层面的横切性逻辑。在实现形式上,既可以是XML文件中配置的普通类,也可以在类代码中用“@Aspect”注解去声明。在运行时,Spring框架会创建类似Advisor来指代它,其内部会包括切入的时机(Pointcut)和切入的动作(Advice)。
  • Join Point,它是Aspect可以切入的特定点,在Spring里面只有方法可以作为Join Point。
  • Advice,它定义了切面中能够采取的动作。如果去看Spring源码,就会发现Advice、Join Point并没有定义在Spring自己的命名空间里,这是因为他们是源自AOP联盟,可以看作是Java工程师在AOP层面沟通的通用规范。

从时序上来看,则可以参考下图,理解具体发生的时机。
在这里插入图片描述
可以参看下面的示意图,来进一步理解上面这些抽象在逻辑上的意义。
在这里插入图片描述

  • Pointcut,它负责具体定义Aspect被应用在哪些Join Point,可以通过指定具体的类名和方法名来实现,或者也可以使用正则表达式来定义条件。
  • Join Point仅仅是可利用的机会。
  • Pointcut是解决了切面编程中的Where问题,让程序可以知道哪些机会点可以应用某个切面动作。
  • 而Advice则是明确了切面编程中的What,也就是做什么;同时通过指定Before、After或者Around,定义了When,也就是什么时候做。

三十八、对比Java标准NIO类库,你知道Netty是如何实现更高性能的吗?

典型回答
单独从性能角度,Netty在基础的NIO等类库之上进行了很多改进,例如:

  • 更加优雅的Reactor模式实现、灵活的线程模型、利用EventLoop等创新性的机制,可以非常高效地管理成百上千的Channel。
  • 充分利用了Java的Zero-Copy机制,并且从多种角度,“斤斤计较”般的降低内存分配和回收的开销。例如,使用池化的Direct Bufer等技术,在提高IO性能的同时,减少了对象的创建和销毁;利用反射等技术直接操纵SelectionKey,使用数组而不是Java容器等。
  • 使用更多本地代码。例如,直接利用JNI调用Open SSL等方式,获得比Java内建SSL引擎更好的性能。
  • 在通信协议、序列化等其他角度的优化。
    总的来说,Netty并没有Java核心类库那些强烈的通用性、跨平台等各种负担,针对性能等特定目标以及Linux等特定环境,采取了一些极致的优化手段。

Netty与Java自身的NIO框架相比有哪些不同呢?
Java的标准类库,由于其基础性、通用性的定位,往往过于关注技术模型上的抽象,而不是从一线应用开发者的角度去思考。引入并发包的一个重要原因就是,应用开发者使用Thread API比较痛苦,需要操心的不仅仅是业务逻辑,而且还要自己负责将其映射到Thread模型上。Java NIO的设计也有类似的特点,开发者需要深入掌握线程、IO、网络等相关概念,学习路径很长,很容易导致代码复杂、晦涩,即使是有经验的工程师,也难以快速地写出高可靠性的实现。

Netty的设计强调了 “Separation Of Concerns”,通过精巧设计的事件机制,将业务逻辑和无关技术逻辑进行隔离,并通过各种方便的抽象,一定程度上填补了了基础平台和业务开发之间的鸿沟,更有利于在应用开发中普及业界的最佳实践。

Netty > java.nio + java. net!

从API能力范围来看,Netty完全是Java NIO框架的一个大大的超集:
在这里插入图片描述
除了核心的事件机制等,Netty还额外提供了很多功能,例如:

  • 从网络协议的角度,Netty除了支持传输层的UDP、TCP、SCTP协议,也支持HTTP(s)、WebSocket等多种应用层协议,它并不是单一协议的API。
  • 在应用中,需要将数据从Java对象转换成为各种应用协议的数据格式,或者进行反向的转换,Netty为此提供了一系列扩展的编解码框架,与应用开发场景无缝衔接,并且性能良好。
  • 它扩展了Java NIO Bufer,提供了自己的ByteBuf实现,并且深度支持Direct Bufer等技术,甚至hack了Java内部对Direct Bufer的分配和销毁等。同时,Netty也提供了更加完善的Scatter/Gather机制实现。

Netty的几个核心概念:

  • ServerBootstrap,服务器端程序的入口,这是Netty为简化网络程序配置和关闭等生命周期管理,所引入的Bootstrapping机制。我们通常要做的创建Channel、绑定端口、注册Handler等,都可以通过这个统一的入口,以Fluent API等形式完成,相对简化了API使用。与之相对应, Bootstrap则是Client端的通常入口。
  • Channel,作为一个基于NIO的扩展框架,Channel和Selector等概念仍然是Netty的基础组件,但是针对应用开发具体需求,提供了相对易用的抽象。
  • EventLoop,这是Netty处理事件的核心机制。例子中使用了EventLoopGroup。我们在NIO中通常要做的几件事情,如注册感兴趣的事件、调度相应的Handler等,都是EventLoop负责。
  • ChannelFuture,这是Netty实现异步IO的基础之一,保证了同一个Channel操作的调用顺序。Netty扩展了Java标准的Future,提供了针对自己场景的特有Future定义。
  • ChannelHandler,这是应用开发者放置业务逻辑的主要地方,也是我上面提到的“Separation Of Concerns”原则的体现。
  • ChannelPipeline,它是ChannelHandler链条的容器,每个Channel在创建后,自动被分配一个ChannelPipeline。在上面的示例中,我们通过ServerBootstrap注册了ChannelInitializer,并且实现了initChannel方法,而在该方法中则承担了向ChannelPipleline安装其他Handler的任务。

在这里插入图片描述

Netty的线程模型是什么样的?
Netty采用Reactor线程模型。这里面主要有三种Reactor线程模型。分别是单线程模式、主从Reactor模式、多Reactor线程模式。其都可以通过初试和EventLoopGroup进行设置。其主要区别在于,单Reactor模式就是一个线程,既进程处理连接,也处理IO。类似于我们传统的OIO编程。主从Reactor模式,其实就是将监听连接和处理IO的分开在不同的线程完成。最后,主从Reactor线程模型,为了解决多Reactor模型下单一线程性能不足的问题。改为了一组线程池进行处理。官方默认的是采用这种主从Reactor模型。其线程数默认为CPU内核的2倍。

三十九、常用的分布式ID的设计方案?

典型回答
首先,我们需要明确通常的分布式ID定义,基本的要求包括:

  • 全局唯一,区别于单点系统的唯一,全局是要求分布式系统内唯一。
  • 有序性,通常都需要保证生成的ID是有序递增的。例如,在数据库存储等场景中,有序ID便于确定数据位置,往往更加高效。

目前业界的方案很多,典型方案包括:

  • 基于数据库自增序列的实现。这种方式优缺点都非常明显,好处是简单易用,但是在扩展性和可靠性等方面存在局限性。
  • 基于Twitter早期开源的Snowfake的实现,以及相关改动方案。这是目前应用相对比较广泛的一种方式,其结构定义你可以参考下面的示意图。

在这里插入图片描述
除了唯一和有序,考虑到分布式系统的功能需要,通常还会额外希望分布式ID保证:

  • 有意义,或者说包含更多信息,例如时间、业务等信息。这一点和有序性要求存在一定关联,如果ID中包含时间,本身就能保证一定程度的有序,虽然并不能绝对保证。ID中包含额外信息,在分布式数据存储等场合中,有助于进一步优化数据访问的效率。
  • 高可用性,这是分布式系统的必然要求。前面谈到的方案中,有的是真正意义上的分布式,有得还是传统主从的思路,这一点没有绝对的对错,取决于我们业务对扩展性、性能等方面的要求。
  • 紧凑性,ID的大小可能受到实际应用的制约,例如数据库存储往往对长ID不友好,太长的ID会降低MySQL等数据库索引的性能、编程语言在处理时也可能受数据类型长度限制。

当前分布式领域的面试热点,例如:

  • 分布式事务,包括其产生原因、业务背景、主流的解决方案等。
  • 理解CAP、BASE等理论,懂得从最终一致性等角度来思考问题,理解Paxos、Raft等一致性算法。
  • 理解典型的分布式锁实现,例如最常见的Redis分布式锁。
  • 负载均衡等分布式领域的典型算法,至少要了解主要方案的原理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值