1、那么请谈谈 AQS 框架是怎么回事儿?
(1)AQS 是 AbstractQueuedSynchronizer 的缩写,它提供了一个 FIFO 队列,可以看成是一个实现同步锁的核心组件。
AQS 是一个抽象类,主要通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取和释放的方法来提供自定义的同步组件。
(2)AQS 的两种功能:独占锁和共享锁
(3)AQS 的内部实现
AQS 的实现依赖内部的同步队列,也就是 FIFO 的双向队列,如果当前线程竞争失败,那么 AQS 会把当前线程以及等待状态信息构造成一个 Node 加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的范文前驱和后继节点。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 AQS 队列中。
2、AQS 对资源的共享方式?
AQS 定义两种资源共享方式
(1)Exclusive(独占)
只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
(2)Share(共享)
多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队 / 唤醒出队等),AQS 已经在顶层实现好了。
3、如何让 Java 的线程彼此同步?
- synchronized
- volatile
- ReenreantLock
- 使用局部变量实现线程同步
4、你了解过哪些同步器?请分别介绍下。
(1)Semaphore 同步器
特征:
经典的信号量,通过计数器控制对共享资源的访问
Semaphore(int count): 创建拥有 count 个许可证的信号量
acquire()/acquire(int num) : 获取 1/num 个许可证
release/release(int num) : 释放 1/num 个许可证
(2)CountDownLatch 同步器
特征:
必须发生指定数量的事件后才可以继续运行 (比如赛跑比赛,裁判喊出 3,2,1 之后大家才同时跑)
CountDownLatch(int count): 必须发生 count 个数量才可以打开锁存器
await: 等待锁存器
countDown: 触发事件
(3)CyclicBarrier 同步器
特征:
适用于只有多个线程都到达预定点时才可以继续执行 (比如斗地主,需要等齐三个人才开始)
CyclicBarrier(int num) : 等待线程的数量
CyclicBarrier(int num, Runnable action) : 等待线程的数量以及所有线程到达后的操作
await() : 到达临界点后暂停线程
(4)交换器 (Exchanger) 同步器
(5)Phaser 同步器
5、Java 中的线程池是如何实现的
创建一个阻塞队列来容纳任务,在第一次执行任务时创建足够多的线程,并处理任务,之后每个工作线程自动从任务队列中获取线程,直到任务队列中任务为 0 为止,此时线程处于等待状态,一旦有工作任务加入任务队列中,即刻唤醒工作线程进行处理,实现线程的可复用性。
线程池一般包括四个基本组成部分:
(1)线程池管理器
用于创建线程池,销毁线程池,添加新任务。
(2)工作线程
线程池中线程,可循环执行任务,在没有任务时处于等待状态。
(3)任务队列
用于存放没有处理的任务,一种缓存机制。
(4)任务接口
每个任务必须实现的接口,供工作线程调度任务的执行,主要规定了任务的开始和收尾工作,和任务的状态。
6、创建线程池的几个核心构造参数
// Java线程池的完整构造函数
public ThreadPoolExecutor(
int corePoolSize, // 线程池长期维持的最小线程数,即使线程处于Idle状态,也不会回收。
int maximumPoolSize, // 线程数的上限
long keepAliveTime, // 线程最大生命周期。
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //任务队列。当线程池中的线程都处于运行状态,而此时任务数量继续增加,则需要一个容器来容纳这些任务,这就是任务队列。
ThreadFactory threadFactory, // 线程工厂。定义如何启动一个线程,可以设置线程名称,并且可以确认是否是后台线程等。
RejectedExecutionHandler handler // 拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。
)
7、线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
线程池中的线程是在第一次提交任务 submit 时创建的
创建线程的方式有继承 Thread 和实现 Runnable,重写 run 方法,start 开始执行,wait 等待,sleep 休眠,shutdown 停止。
(1)newSingleThreadExecutor:单线程池。
顾名思义就是一个池中只有一个线程在运行,该线程永不超时,而且由于是一个线程,当有多个任务需要处理时,会将它们放置到一个无界阻塞队列中逐个处理,它的实现代码如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable()));
}
它的使用方法也很简单,下面是简单的示例:
public static void main(String[] args) throws ExecutionException,InterruptedException {
// 创建单线程执行器
ExecutorService es = Executors.newSingleThreadExecutor();
// 执行一个任务
Future<String> future = es.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "";
}
});
// 获得任务执行后的返回值
System.out.println("返回值:" + future.get());
// 关闭执行器
es.shutdown();
}
(2)newCachedThreadPool:缓冲功能的线程。
建立了一个线程池,而且线程数量是没有限制的 (当然,不能超过 Integer 的最大值),新增一个任务即有一个线程处理,或者复用之前空闲的线程,或者重亲启动一个线程,但是一旦一个线程在 60 秒内一直处于等待状态时(也就是一分钟无事可做),则会被终止,其源码如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这里需要说明的是,任务队列使用了同步阻塞队列,这意味着向队列中加入一个元素,即可唤醒一个线程 (新创建的线程或复用空闲线程来处理),这种队列已经没有队列深度的概念了。
(3)newFixedThreadPool:固定线程数量的线程池。
在初始化时已经决定了线程的最大数量,若任务添加的能力超出了线程的处理能力,则建立阻塞队列容纳多余的任务,其源码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
上面返回的是一个 ThreadPoolExecutor,它的 corePoolSize 和 maximumPoolSize 是相等的,也就是说,最大线程数量为 nThreads。如果任务增长的速度非常快,超过了 LinkedBlockingQuene 的最大容量 (Integer 的最大值),那此时会如何处理呢?会按照 ThreadPoolExecutor 默认的拒绝策略(默认是 DiscardPolicy,直接丢弃) 来处理。
以上三种线程池执行器都是 ThreadPoolExecutor 的简化版,目的是帮助开发人员屏蔽过得线程细节,简化多线程开发。当需要运行异步任务时,可以直接通过 Executors 获得一个线程池,然后运行任务,不需要关注 ThreadPoolExecutor 的一系列参数时什么含义。当然,有时候这三个线程不能满足要求,此时则可以直接操作 ThreadPoolExecutor 来实现复杂的多线程计算。
newSingleThreadExecutor、newCachedThreadPool、newFixedThreadPool 是线程池的简化版,而 ThreadPoolExecutor 则是旗舰版___简化版容易操作,需要了解的知识相对少些,方便使用,而旗舰版功能齐全,适用面广,难以驾驭。
8、volatile 关键字的作用
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
volatile 常用于多线程环境下的单次操作 (单次读或者单次写)。
9、既然 volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?
volatile 修饰的变量在各个线程的工作内存中不存在一致性的问题(在各个线程工作的内存中,volatile 修饰的变量也会存在不一致的情况,但是由于每次使用之前都会先刷新主存中的数据到工作内存,执行引擎看不到不一致的情况,因此可以认为不存在不一致的问题),但是 java 的运算并非原子性的操作,导致 volatile 在并发下并非是线程安全的。
10、ThreadLocal 是什么?有哪些使用场景?
ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。
原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B 线程正在使用的 Connection; 还有 Session 管理 等问题。
11、请谈谈 ThreadLocal 是怎么解决并发安全的?
在 java 程序中,常用的有两种机制来解决多线程并发问题,一种是 sychronized 方式,通过锁机制,一个线程执行时,让另一个线程等待,是以时间换空间的方式来让多线程串行执行。而另外一种方式就是 ThreadLocal 方式,通过创建线程局部变量,以空间换时间的方式来让多线程并行执行。两种方式各有优劣,适用于不同的场景,要根据不同的业务场景来进行选择。
在 spring 的源码中,就使用了 ThreadLocal 来管理连接,在很多开源项目中,都经常使用 ThreadLocal 来控制多线程并发问题,因为它足够的简单,我们不需要关心是否有线程安全问题,因为变量是每个线程所特有的。
12、很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?
ThreadLocal 变量解决了多线程环境下单个线程中变量的共享问题,使用名为 ThreadLocalMap 的哈希表进行维护(key 为 ThreadLocal 变量名,value 为 ThreadLocal 变量的值);
使用时需要注意以下几点:
- 线程之间的 threadLocal 变量是互不影响的,
- 使用 private final static 进行修饰,防止多实例时内存的泄露问题
- 线程池环境下使用后将 threadLocal 变量 remove 掉或设置成一个初始值
13、为什么代码会重排序?
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
- 在单线程环境下不能改变程序运行的结果;
- 存在数据依赖关系的不允许重排序
需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。
14、什么是自旋
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
15、多线程中 synchronized 锁升级的原理是什么?
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
16、synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
相同点:两者都是可重入锁
两者都是可重入锁。“可重入锁” 概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
主要区别如下:
- ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
- 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中 mark word
Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的 class 对象
- 同步方法块,锁是括号里面的对象
17、Java Concurrency API 中的 Lock 接口 (Lock interface) 是什么?对比同步它有什么优势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有:
(1)可以使锁更公平
(2)可以使线程在等待锁的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的 (tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法) 锁操作。另外 Lock 的实现类基本都支持非公平锁 (默认) 和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
18、jsp 和 servlet 有什么区别?
(1)servlet 是服务器端的 Java 程序,它担当客户端和服务端的中间层。
(2)jsp 全名为 Java server pages,中文名叫 Java 服务器页面,其本质是一个简化的 servlet 设计。JSP 是一种动态页面设计,它的主要目的是将表示逻辑从 servlet 中分离出来。
(3)JVM 只能识别 Java 代码,不能识别 JSP,JSP 编译后变成了 servlet,web 容器将 JSP 的代码编译成 JVM 能够识别的 Java 类(servlet)。
(4)JSP 有内置对象、servlet 没有内置对象。
19、jsp 有哪些内置对象?作用分别是什么?
JSP 九大内置对象:
- pageContext,页面上下文对象,相当于页面中所有功能的集合,通过它可以获取 JSP 页面的 out、request、response、session、application 对象。
- request
- response
- session
- application,应用程序对象,application 实现了用户间数据的共享,可存放全局变量,它开始于服务器启动,知道服务器关闭。
- page,就是 JSP 本身。
- exception
- out,out 用于在 web 浏览器内输出信息,并且管理应用服务器上的输出缓冲区,作用域 page。
- config,取得服务器的配置信息。
20、forward 和 redirect 的区别?
- forward 是直接请求转发;redirect 是间接请求转发,又叫重定向。
- forward,客户端和浏览器执行一次请求;redirect,客户端和浏览器执行两次请求。
- forward,经典的 MVC 模式就是 forward;redirect,用于避免用户的非正常访问。(例如用户非正常访问,servlet 就可以将 HTTP 请求重定向到登录页面)。
- forward,地址不变;redirect,地址改变。
- forward 常用方法:RequestDispatcher 类的 forward() 方法;redirect 常用方法:HttpServletRequest 类的 sendRedirect() 方法。
21、说一下 jsp 的 4 种作用域?
application、session、request、page
22、session 和 cookie 有什么区别?
(1)存储位置不同
- cookie 在客户端浏览器;
- session 在服务器;
(2)存储容量不同
- cookie<=4K,一个站点最多保留 20 个 cookie;
- session 没有上线,出于对服务器的保护,session 内不可存过多东西,并且要设置 session 删除机制;
(3)存储方式不同
- cookie 只能保存 ASCII 字符串,并需要通过编码方式存储为 Unicode 字符或者二进制数据;
- session 中能存储任何类型的数据,包括并不局限于 String、integer、list、map 等;
(4)隐私策略不同
- cookie 对客户端是可见的,不安全;
- session 存储在服务器上,安全;
(5)有效期不同
- 开发可以通过设置 cookie 的属性,达到使 cookie 长期有效的效果;
- session 依赖于名为 JESSIONID 的 cookie,而 cookie JSESSIONID 的过期时间默认为 - 1,只需关闭窗口该 session 就会失效,因而 session 达不到长期有效的效果;
(6)跨域支持上不同
- cookie 支持跨域;
- session 不支持跨域;
23、如果客户端禁止 cookie 能实现 session 还能用吗?
一般默认情况下,在会话中,服务器存储 session 的 sessionid 是通过 cookie 存到浏览器里。
如果浏览器禁用了 cookie,浏览器请求服务器无法携带 sessionid,服务器无法识别请求中的用户身份,session 失效。
但是可以通过其他方法在禁用 cookie 的情况下,可以继续使用 session。
- 通过 url 重写,把 sessionid 作为参数追加的原 url 中,后续的浏览器与服务器交互中携带 sessionid 参数。
- 服务器的返回数据中包含 sessionid,浏览器发送请求时,携带 sessionid 参数。
- 通过 Http 协议其他 header 字段,服务器每次返回时设置该 header 字段信息,浏览器中 js 读取该 header 字段,请求服务器时,js 设置携带该 header 字段。
24、什么是上下文切换?
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
25、cookie、session、token
1、session 机制
session 是服务端存储的一个对象,主要用来存储所有访问过该服务端的客户端的用户信息(也可以存储其他信息),从而实现保持用户会话状态。但是服务器重启时,内存会被销毁,存储的用户信息也就消失了。
不同的用户访问服务端的时候会在 session 对象中存储键值对,“键”用来存储开启这个用户信息的 “钥匙”,在登录成功后,“钥匙” 通过 cookie 返回给客户端,客户端存储为 sessionId 记录在 cookie 中。当客户端再次访问时,会默认携带 cookie 中的 sessionId 来实现会话机制。
(1)session 是基于 cookie 的。
- cookie 的数据 4k 左右;
- cookie 存储数据的格式:字符串 key=value
- cookie 存储有效期:可以自行通过 expires 进行具体的日期设置,如果没设置,默认是关闭浏览器时失效。
- cookie 有效范围:当前域名下有效。所以 session 这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上(前后端项目协议、域名、端口号都一致,即在一个项目下)
(2)session 持久化
用于解决重启服务器后 session 消失的问题。在数据库中存储 session,而不是存储在内存中。通过包:express-mysql-session。
当客户端存储的 cookie 失效后,服务端的 session 不会立即销毁,会有一个延时,服务端会定期清理无效 session,不会造成无效数据占用存储空间的问题。
2、token 机制
适用于前后端分离的项目(前后端代码运行在不同的服务器下)
请求登录时,token 和 sessionid 原理相同,是对 key 和 key 对应的用户信息进行加密后的加密字符,登录成功后,会在响应主体中将 {token:“字符串”} 返回给客户端。
客户端通过 cookie 都可以进行存储。再次请求时不会默认携带,需要在请求拦截器位置给请求头中添加认证字段 Authorization 携带 token 信息,服务器就可以通过 token 信息查找用户登录状态。
26、说一下 session 的工作原理?
当客户端登录完成后,会在服务端产生一个 session,此时服务端会将 sessionid 返回给客户端浏览器。客户端将 sessionid 储存在浏览器的 cookie 中,当用户再次登录时,会获得对应的 sessionid,然后将 sessionid 发送到服务端请求登录,服务端在内存中找到对应的 sessionid,完成登录,如果找不到,返回登录页面。
27、http 响应码 301 和 302 代表的是什么?有什么区别?
- 301 和 302 状态码都表示重定向,当浏览器拿到服务器返回的这个状态码后悔自动跳转到一个新的 URL 地址。
- 301 代表永久性重定向,旧地址被永久移除,客户端向新地址发送请求。
- 302 代表暂时性重定向,旧地址还在,客户端继续向旧地址发送请求。
- 303 代表暂时性重定向,重定向到新地址时,必须使用 GET 方法请求新地址。
- 307 代表暂时性重定向,与 302 的区别在于 307 不允许从 POST 改为 GET。
- 307 代表永久性重定向,与 301 的区别在于 308 不允许从 POST 改为 GET。
28、简述 tcp 和 udp 的区别?
- TCP 是传输控制协议,UDP 是用户数据表协议;
- TCP 长连接,UDP 无连接;
- UDP 程序结构较简单,只需发送,无须接收;
- TCP 可靠,保证数据正确性、顺序性;UDP 不可靠,可能丢数据;
- TCP 适用于少量数据,UDP 适用于大量数据传输;
- TCP 速度慢,UDP 速度快;
29、tcp 为什么要三次握手,两次不行吗?为什么?
因为客户端和服务端都要确认连接,①客户端请求连接服务端;②针对客户端的请求确认应答,并请求建立连接;③针对服务端的请求确认应答,建立连接;
两次无法确保 A 能收到 B 的数据;
30、OSI 的七层模型都有哪些?
31、get 和 post 请求有哪些区别?
- get 请求参数是连接在 url 后面的, 而 post 请求参数是存放在 requestbody 内的;
- get 请求因为浏览器对 url 长度有限制,所以参数个数有限制,而 post 请求参数个数没有限制;
- 因为 get 请求参数暴露在 url 上, 所以安全方面 post 比 get 更加安全;
- get 请求只能进行 url 编码, 而 post 请求可以支持多种编码方式;
- get 请求参数会保存在浏览器历史记录内, post 请求并不会;
- get 请求浏览器会主动 cache,post 并不会, 除非主动设置;
- get 请求产生 1 个 tcp 数据包, post 请求产生 2 个 tcp 数据包;
- 在浏览器进行回退操作时, get 请求是无害的, 而 post 请求则会重新请求一次;
- 浏览器在发送 get 请求时会将 header 和 data 一起发送给服务器, 服务器返回 200 状态码, 而在发送 post 请求时, 会先将 header 发送给服务器, 服务器返回 100, 之后再将 data 发送给服务器, 服务器返回 200 OK;
32、什么是 XSS 攻击,如何避免?
xss(Cross Site Scripting),即跨站脚本攻击,是一种常见于 web 应用程序中的计算机安全漏洞。指的是在用户浏览器上,在渲染 DOM 树的时候,执行了不可预期的 JS 脚本,从而发生了安全问题。
XSS 就是通过在用户端注入恶意的可运行脚本,若服务端对用户的输入不进行处理,直接将用户的输入输出到浏览器,然后浏览器将会执行用户注入的脚本。 所以 XSS 攻击的核心就是浏览器渲染 DOM 的时候将文本信息解析成 JS 脚本从而引发 JS 脚本注入,那么 XSS 攻击的防御手段就是基于浏览器渲染这一步去做防御。只要我们使用 HTML 编码将浏览器需要渲染的信息编码后,浏览器在渲染 DOM 元素的时候,会自动解码需要渲染的信息,将上述信息解析成字符串而不是 JS 脚本,这就是我们防御 XSS 攻击的核心想法。
预防:
1、获取用户的输入,不用 innerHtml, 用 innerText.
2、对用户的输入进行过滤,如对 & < > " ’ / 等进行转义;
33、什么是 CSRF 攻击,如何避免?
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
1、攻击细节
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
例子
假如一家银行用以运行转账操作的 URL 地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成信息的网站中。这意味着如果服务端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义运行操作。
2、防御措施
检查 Referer 字段
HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。以上文银行操作为例,Referer 字段地址通常应该是转账按钮所在的网页地址,应该也位于 www.examplebank.com 之下。而如果是 CSRF 攻击传来的请求,Referer 字段会是包含恶意网址的地址,不会位于 www.examplebank.com 之下,这时候服务器就能识别出恶意的访问。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 http 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
3、添加校验 token
由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行 CSRF 攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验 token 的值为空或者错误,拒绝这个可疑请求。
34、如何实现跨域?说一下 JSONP 实现原理?
1、jsonp 原理详解——终于搞清楚 jsonp 是啥了
2、最流行的跨域方案 cors
cors 是目前主流的跨域解决方案,跨域资源共享 (CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
3、最方便的跨域方案 Nginx
nginx 是一款极其强大的 web 服务器,其优点就是轻量级、启动快、高并发。
现在的新项目中 nginx 几乎是首选,我们用 node 或者 java 开发的服务通常都需要经过 nginx 的反向代理。
反向代理的原理很简单,即所有客户端的请求都必须先经过 nginx 的处理,nginx 作为代理服务器再讲请求转发给 node 或者 java 服务,这样就规避了同源策略。
35、websocket 应用的是哪个协议
WebSocket 是一个允许 Web 应用程序 (通常指浏览器) 与服务器进行双向通信的协议。HTML5 的 WebSocket API 主要是为浏览器端提供了一个基于 TCP 协议实现全双工通信的方法。
WebSocket 优势: 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,双方可以在任意时刻,相互推送信息。同时,服务器与客户端之间交换的头信息很小。
36、说一下 tcp 粘包是怎么产生的?
发送方需要等缓冲区满才能发送出去,造成粘包;
接收方不及时接收缓冲区的包,造成粘包;
37、请列举出在 JDK 中几个常用的设计模式?
1、单例模式
作用:保证类只有一个实例。
JDK 中体现:Runtime 类。
2、静态工厂模式
作用:代替构造函数创建对象,方法名比构造函数清晰。
JDK 中体现:Integer.valueOf、Class.forName
3、抽象工厂
作用:创建某一种类的对象。
JDK 中体现:Java.sql 包。
4、原型模式
clone();
原型模式的本质是拷贝原型来创建新的对象,拷贝是比 new 更快的创建对象的方法,当需要大批量创建新对象而且都是同一个类的对象的时候考虑使用原型模式。
一般的克隆只是浅拷贝(对象的 hash 值不一样,但是对象里面的成员变量的 hash 值是一样的)。
有些场景需要深拷贝,这时我们就要重写 clone 方法,以 ArrayList 为例:
5、适配器模式
作用:使不兼容的接口相容。
JDK 中体现:InputStream、OutputStream。
6、装饰器模式
作用:为类添加新的功能,防止类继承带来的类爆炸。
JDK 中体现:io 类、Collections、List。
7、外观模式
作用:封装一组交互类,一直对外提供接口。
JDK 中体现:logging 包。
8、享元模式
作用:共享对象、节省内存。
JDK 中体现:Integer.valueOf、String 常量池。
9、代理模式
作用:
(1)透明调用被代理对象,无须知道复杂实现细节;
(2)增加被代理类的功能;
JDK 中体现:动态代理。
10、迭代器模式
作用:将集合的迭代和集合本身分离。
JDK 中体现:Iterator
11、命令模式
作用:封装操作,使接口一致。
JDK 中体现:Runable、Callable、ThreadPoolExecutor。
38、什么是设计模式?你是否在你的代码里面使用过任何设计模式?
1、什么是设计模式?
设计模式是解决软件开发某些特定问题而提出的一些解决方案,也可以理解为解决问题的一些固定思路。
通过设计模式可以帮助我们增强代码的可复用性、可扩展性、灵活性。
我们使用设计模式的最终目的是实现代码的高内聚、低耦合。
2、设计模式的七大原则
- 单一职责原则
- 接口隔离原则
- 依赖倒转原则
- 里式替换原则
- 开闭原则
- 迪米特法则
- 合成复用原则
3、你是否在你的代码里面使用过任何设计模式?
(1)单例模式
JDK 种的 runtime,Spring 种的 singeton。
(2)简单工厂模式
Spring 的 BeanFactory,根据传入一个唯一标识来获得 bean 对象。
(3)原型模式
clone()
(4)代理模式
Spring 的 AOP 中,Spring 实现 AOP 功能的原理就是代理模式,①JDK 动态代理。②CGLIB 动态代理,使用 Advice(通知)对类进行方法级别的切面增强。
(5)装饰器模式
为类添加新的功能,防止类爆炸;
IO 流、数据源包装,Spring 中用到的装饰器模式表现在 Wrapper。
39、Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
- 保证程序只有一个对象的实例,叫做单例模式;
- 内部类的方式实现单例模式,是线程安全的;
- 双重验证方式实现单例模式也是线程安全的;
40、在 Java 中,什么叫观察者设计模式(observer design pattern)?
1、观察者模式是一种一对多的依赖关系,让多个观察者同时监听某一主题对象。当这个主题对象发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
2、JAVA 提供的对观察者模式的支持
在 JAVA 语言的 java.util 库里面,提供了一个 Observable 类以及一个 Observer 接口,构成 JAVA 语言对观察者模式的支持。
(1)Observer 接口
这个接口只定义了一个方法,即 update() 方法,当被观察者对象的状态发生变化时,被观察者对象的 notifyObservers() 方法就会调用这一方法。
public interface Observer {
void update(Observable o, Object arg);
}
- 1
- 2
- 3
(2)Observable 类
被观察者类都是 java.util.Observable 类的子类。java.util.Observable 提供公开的方法支持观察者对象,这些方法中有两个对 Observable 的子类非常重要:一个是 setChanged(),另一个是 notifyObservers()。第一方法 setChanged() 被调用之后会设置一个内部标记变量,代表被观察者对象的状态发生了变化。第二个是 notifyObservers(),这个方法被调用时,会调用所有登记过的观察者对象的 update() 方法,使这些观察者对象可以更新自己。
41、使用工厂模式最主要的好处是什么?在哪里使用?
1、工厂模式好处
- 良好的封装性、代码结构清晰;
- 扩展性好,如果想增加一个产品,只需扩展一个工厂类即可;
- 典型的解耦框架;
2、在哪里使用?
- 需要生成对象的地方;
- 不同数据库的访问;
42、请解释自动装配模式的区别?
有五种自动装配的方式,可以用来指导 Spring 容器用自动装配方式来进行依赖注入。
1、no
默认的方式是不进行自动装配,通过显式设置 ref 属性来进行装配。第 402 页 共 485 页
2、byName
通过参数名 自动装配,Spring 容器在配置文件中发现 bean
的 autowire 属性被设置成 byname,之后容器试图匹配、装配和该 bean 的属
性具有相同名字的 bean。
3、byType:
通过参数类型自动装配,Spring 容器在配置文件中发现 bean
的 autowire 属性被设置成 byType,之后容器试图匹配、装配和该 bean 的属
性具有相同类型的 bean。如果有多个 bean 符合条件,则抛出错误。
4、constructor
这个方式类似于 byType, 但是要提供给构造器参数,如
果没有确定的带参数的构造器参数类型,将会抛出异常。
5、autodetect
首先尝试使用 constructor 来自动装配,如果无法工作,
则使用 byType 方式。
43、举一个用 Java 实现的装饰模式 (decorator design pattern)?它是作用于对象层次还是类层次?
在 Java IO 中运用了装饰器模式,inputStream 作为抽象类,其下有几个实现类,表示从不同的数据源输入:
- byteArrayInputStream
- fileInputStream
- StringBufferInputStream
- PipedInputStream,从管道产生输入;
- SequenceInputStream,可将其他流收集合并到一个流内;
FilterInputStream 作为装饰器在 JDK 中是一个普通类,其下面有多个具体装饰器比如 BufferedInputStream、DataInputStream 等。
FilterInputStream 内部封装了基础构件:
protected volatile InputStream in;
而 BufferedInputStream 在调用其 read() 读取数据时会委托基础构件来进行更底层的操作,而它自己所起的装饰作用就是缓冲,在源码中可以很清楚的看到这一切。
44、什么是 Spring 框架?Spring 框架有哪些主要模块?
Spring 是一个控制反转和面向切面的容器框架。
Spring 有七大功能模块:
1、Core
Core 模块是 Spring 的核心类库,Core 实现了 IOC 功能。
2、AOP
Apring AOP 模块是 Spring 的 AOP 库,提供了 AOP(拦截器)机制,并提供常见的拦截器,供用户自定义和配置。
3、orm
提供对常用 ORM 框架的管理和支持,hibernate、mybatis 等。
4、Dao
Spring 提供对 JDBC 的支持,对 JDBC 进行封装。
5、Web
对 Struts2 的支持。
6、Context
Context 模块提供框架式的 Bean 的访问方式,其它程序可以通过 Context 访问 Spring 的 Bean 资源,相当于资源注入。
7、MVC
MVC 模块为 spring 提供了一套轻量级的 MVC 实现,即 Spring MVC。
45、使用 Spring 框架能带来哪些好处?
1、轻量级框架、容器
Spring 是一个容器,管理对象的生命周期和配置。基于一个可配置原型 prototype,你的 bean 可以使单利的,也可以每次需要时都生成一个新的实例。
2、控制反转 IOC
Spring 通过控制反转实现松耦合。
3、支持 AOP
Spring 提供对 AOP 的支持,它允许将一些通用任务,如安全、事务、日志等进行集中式处理,从而提高了程序的复用性。
4、轻量级框架
5、方便测试
Spring 提供 Junit4 的支持,可以通过注解方便测试 spring 程序。
6、对 Java 中很多 API 进行了封装
7、方便集成各种优秀框架
如 Struts、hibernate、mybstis。
8、支持声明式事务处理
只需通过配置就可以完成对事务的管理,而无须手动编程。
46、Spring IOC、AOP 举例说明
1、IOC 理论的背景
我们都知道,在采用面向对象方法设计的软件系统中,它的底层实现都是由 N 个对象组成的,所有的对象通过彼此的合作,最终实现系统的业务逻辑。
如果我们打开机械式手表的后盖,就会看到与上面类似的情形,各个齿轮分别带动时针、分针和秒针顺时针旋转,从而在表盘上产生正确的时间。图 1 中描述的就是这样的一个齿轮组,它拥有多个独立的齿轮,这些齿轮相互啮合在一起,协同工作,共同完成某项任务。我们可以看到,在这样的齿轮组中,如果有一个齿轮出了问题,就可能会影响到整个齿轮组的正常运转。
齿轮组中齿轮之间的啮合关系, 与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。现在,伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。
耦合关系不仅会出现在对象与对象之间,也会出现在软件系统的各模块之间,以及软件系统和硬件系统之间。如何降低系统之间、模块之间和对象之间的耦合度,是软件工程永远追求的目标之一。为了解决对象之间的耦合度过高的问题,软件专家 Michael Mattson 提出了 IOC 理论,用来实现对象之间的 “解耦”,目前这个理论已经被成功地应用到实践当中,很多的 J2EE 项目均采用了 IOC 框架产品 Spring。
2、什么是控制反转
IOC 是 Inversion of Control 的缩写,多数书籍翻译成 “控制反转”,还有些书籍翻译成为“控制反向” 或者“控制倒置”。
1996 年,Michael Mattson 在一篇有关探讨面向对象框架的文章中,首先提出了 IOC 这个概念。对于面向对象设计及编程的基本思想,前面我们已经讲了很多了,不再赘述,简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。IOC 理论提出的观点大体是这样的:借助于 “第三方” 实现具有依赖关系的对象之间的解耦,如下图:
大家看到了吧,由于引进了中间位置的 “第三方”,也就是 IOC 容器,使得 A、B、C、D 这 4 个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方” 了,全部对象的控制权全部上缴给 “第三方”IOC 容器,所以,IOC 容器成了整个系统的关键核心,它起到了一种类似“粘合剂” 的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个 “粘合剂”,对象与对象之间会彼此失去联系,这就是有人把 IOC 容器比喻成“粘合剂” 的由来。
我们再来做个试验:把上图中间的 IOC 容器拿掉,然后再来看看这套系统(拿掉 IoC 容器后的系统):
我们现在看到的画面,就是我们要实现整个系统所需要完成的全部内容。这时候,A、B、C、D 这 4 个对象之间已经没有了耦合关系,彼此毫无联系,这样的话,当你在实现 A 的时候,根本无须再去考虑 B、C 和 D 了,对象之间的依赖关系已经降低到了最低程度。所以,如果真能实现 IOC 容器,对于系统开发而言,这将是一件多么美好的事情,参与开发的每一成员只要实现自己的类就可以了,跟别人没有任何关系!
我们再来看看,控制反转 (IOC) 到底为什么要起这么个名字?我们来对比一下:
软件系统在没有引入 IOC 容器之前,如图 1 所示,对象 A 依赖于对象 B,那么对象 A 在初始化或者运行到某一点的时候,自己必须主动去创建对象 B 或者使用已经创建的对象 B。无论是创建还是使用对象 B,控制权都在自己手上。
软件系统在引入 IOC 容器之后,这种情形就完全改变了,如图 3 所示,由于 IOC 容器的加入,对象 A 与对象 B 之间失去了直接联系,所以,当对象 A 运行到需要对象 B 的时候,IOC 容器会主动创建一个对象 B 注入到对象 A 需要的地方。
通过前后的对比,我们不难看出来:对象 A 获得依赖对象 B 的过程, 由主动行为变为了被动行为,控制权颠倒过来了,这就是 “控制反转” 这个名称的由来。
3、IOC 的别名:依赖注入(DI)
2004 年,Martin Fowler 探讨了同一个问题,既然 IOC 是控制反转,那么到底是 “哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由 IOC 容器主动注入。于是,他给“控制反转” 取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。他的这个答案,实际上给出了实现 IOC 的方法:注入。所谓依赖注入,就是由 IOC 容器在运行期间,动态地将某种依赖关系注入到对象之中。
所以,依赖注入 (DI) 和控制反转 (IOC) 是从不同的角度的描述的同一件事情,就是指通过引入 IOC 容器,利用依赖关系注入的方式,实现对象之间的解耦。
我们举一个生活中的例子,来帮助理解依赖注入的过程。大家对 USB 接口和 USB 设备应该都很熟悉吧,USB 为我们使用电脑提供了很大的方便,现在有很多的外部设备都支持 USB 接口。
现在,我们利用电脑主机和 USB 接口来实现一个任务:从外部 USB 设备读取一个文件。
电脑主机读取文件的时候,它一点也不会关心 USB 接口上连接的是什么外部设备,而且它确实也无须知道。它的任务就是读取 USB 接口,挂接的外部设备只要符合 USB 接口标准即可。所以,如果我给电脑主机连接上一个 U 盘,那么主机就从 U 盘上读取文件;如果我给电脑主机连接上一个外置硬盘,那么电脑主机就从外置硬盘上读取文件。挂接外部设备的权力由我作主,即控制权归我,至于 USB 接口挂接的是什么设备,电脑主机是决定不了,它只能被动的接受。电脑主机需要外部设备的时候,根本不用它告诉我,我就会主动帮它挂上它想要的外部设备,你看我的服务是多么的到位。这就是我们生活中常见的一个依赖注入的例子。在这个过程中,我就起到了 IOC 容器的作用。
通过这个例子, 依赖注入的思路已经非常清楚:当电脑主机读取文件的时候,我就把它所要依赖的外部设备,帮他挂接上。整个外部设备注入的过程和一个被依赖的对象在系统运行时被注入另外一个对象内部的过程完全一样。
我们把依赖注入应用到软件系统中,再来描述一下这个过程:
对象 A 依赖于对象 B, 当对象 A 需要用到对象 B 的时候,IOC 容器就会立即创建一个对象 B 送给对象 A。IOC 容器就是一个对象制造工厂,你需要什么,它会给你送去,你直接使用就行了,而再也不用去关心你所用的东西是如何制成的,也不用关心最后是怎么被销毁的,这一切全部由 IOC 容器包办。
在传统的实现中,由程序内部代码来控制组件之间的关系。我们经常使用 new 关键字来实现两个组件之间关系的组合,这种实现方式会造成组件之间耦合。IOC 很好地解决了该问题,它将实现组件间关系从程序内部提到外部容器,也就是说由容器在运行期将组件间的某种依赖关系动态注入组件中。
4、IOC 为我们带来了什么好处
我们还是从 USB 的例子说起,使用 USB 外部设备比使用内置硬盘,到底带来什么好处?
第一、USB 设备作为电脑主机的外部设备,在插入主机之前,与电脑主机没有任何的关系,只有被我们连接在一起之后,两者才发生联系,具有相关性。所以,无论两者中的任何一方出现什么的问题,都不会影响另一方的运行。这种特性体现在软件工程中,就是可维护性比较好,非常便于进行单元测试,便于调试程序和诊断故障。代码中的每一个 Class 都可以单独测试,彼此之间互不影响,只要保证自身的功能无误即可,这就是组件之间低耦合或者无耦合带来的好处。
第二、USB 设备和电脑主机的之间无关性,还带来了另外一个好处,生产 USB 设备的厂商和生产电脑主机的厂商完全可以是互不相干的人,各干各事,他们之间唯一需要遵守的就是 USB 接口标准。这种特性体现在软件开发过程中,好处可是太大了。每个开发团队的成员都只需要关心实现自身的业务逻辑,完全不用去关心其它的人工作进展,因为你的任务跟别人没有任何关系,你的任务可以单独测试,你的任务也不用依赖于别人的组件,再也不用扯不清责任了。所以,在一个大中型项目中,团队成员分工明确、责任明晰,很容易将一个大的任务划分为细小的任务,开发效率和产品质量必将得到大幅度的提高。
第三、同一个 USB 外部设备可以插接到任何支持 USB 的设备,可以插接到电脑主机,也可以插接到 DV 机,USB 外部设备可以被反复利用。在软件工程中,这种特性就是可复用性好,我们可以把具有普遍性的常用组件独立出来,反复利用到项目中的其它部分,或者是其它项目,当然这也是面向对象的基本特征。显然,IOC 不仅更好地贯彻了这个原则,提高了模块的可复用性。符合接口标准的实现,都可以插接到支持此标准的模块中。
第四、同 USB 外部设备一样,模块具有热插拔特性。IOC 生成对象的方式转为外置方式,也就是把对象生成放在配置文件里进行定义,这样,当我们更换一个实现子类将会变得很简单,只要修改配置文件就可以了,完全具有热插拨的特性。
以上几点好处,难道还不足以打动我们,让我们在项目开发过程中使用 IOC 框架吗?
5、IOC 容器的技术剖析
IOC 中最基本的技术就是 “反射(Reflection)” 编程,目前. Net C#、Java 和 PHP5 等语言均支持,其中 PHP5 的技术书籍中,有时候也被翻译成 “映射”。有关反射的概念和用法,大家应该都很清楚,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象。这种编程方式可以让对象在生成时才决定到底是哪一种对象。反射的应用是很广泛的,很多的成熟的框架,比如象 Java 中的 Hibernate、Spring 框架,.Net 中 NHibernate、Spring.Net 框架都是把“反射” 做为最基本的技术手段。
反射技术其实很早就出现了,但一直被忽略,没有被进一步的利用。当时的反射编程方式相对于正常的对象生成方式要慢至少得 10 倍。现在的反射技术经过改良优化,已经非常成熟,反射方式生成对象和通常对象生成方式,速度已经相差不大了,大约为 1-2 倍的差距。
我们可以把 IOC 容器的工作模式看做是工厂模式的升华,可以把 IOC 容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言的的反射编程,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC 是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。
6、IOC 容器的一些产品
Sun ONE 技术体系下的 IOC 容器有:轻量级的有 Spring、Guice、Pico Container、Avalon、HiveMind;重量级的有 EJB;不轻不重的有 JBoss,Jdon 等等。Spring 框架作为 Java 开发中 SSH(Struts、Spring、Hibernate) 三剑客之一,大中小项目中都有使用,非常成熟,应用广泛,EJB 在关键性的工业级项目中也被使用,比如某些电信业务。
.Net 技术体系下的 IOC 容器有:Spring.Net、Castle 等等。Spring.Net 是从 Java 的 Spring 移植过来的 IOC 容器,Castle 的 IOC 容器就是 Windsor 部分。它们均是轻量级的框架,比较成熟,其中 Spring.Net 已经被逐渐应用于各种项目中。
7、使用 IOC 框架应该注意什么
使用 IOC 框架产品能够给我们的开发过程带来很大的好处,但是也要充分认识引入 IOC 框架的缺点,做到心中有数,杜绝滥用框架。
(1)软件系统中由于引入了第三方 IOC 容器,生成对象的步骤变得有些复杂,本来是两者之间的事情,又凭空多出一道手续,所以,我们在刚开始使用 IOC 框架的时候,会感觉系统变得不太直观。所以,引入了一个全新的框架,就会增加团队成员学习和认识的培训成本,并且在以后的运行维护中,还得让新加入者具备同样的知识体系。
(2)由于 IOC 容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。
(3)、具体到 IOC 框架产品 (比如:Spring) 来讲,需要进行大量的配制工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
(4)IOC 框架产品本身的成熟度需要进行评估,如果引入一个不成熟的 IOC 框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。
我们大体可以得出这样的结论:一些工作量不大的项目或者产品,不太适合使用 IOC 框架产品。另外,如果团队成员的知识能力欠缺,对于 IOC 框架产品缺乏深入的理解,也不要贸然引入。最后,特别强调运行效率的项目或者产品,也不太适合引入 IOC 框架产品,象 WEB2.0 网站就是这种情况。
47、什么是控制反转 (IOC)?什么是依赖注入?
借助 Spring 实现具有依赖关系的对象之间的解耦。
对象 A 运行需要对象 B,由主动创建变为 IOC 容器注入,这便是控制反转。
获得依赖对象的过程被反转了,获取依赖对象的过程由自身创建变为由 IOC 容器注入,这便是依赖注入。
48、BeanFactory 和 ApplicationContext 有什么区别?
1、BeanFactory 是 Spring 的最底层接口,包含 bean 的定义,管理 bean 的加载,实例化,控制 bean 的生命周期,特点是每次获取对象时才会创建对象。
ApplicationContext 是 BeanFactory 的子接口,拥有 BeanFactory 的全部功能,并且扩展了很多高级特性,每次容器启动时就会创建所有的对象。
- ApplicationContext 的额外功能:
- 继承 MessageSource,支持国际化;
- 统一的资源文件访问方式;
- 提供在监听器中注册 bean;
- 同时加载过个配置文件;
- 载入多个(有继承关系)上下文,使得每个上下文都专注于一个特定的层次,比如应用的 web 层;
2、BeanFactory 通常以编程的方式被创建,ApplicationContext 可以以声明的方式创建,如使用 ContextLoader。
3、BeanFactory 和 ApplicationContext 都支持 BeanPostProcessor,BeanFactoryPostProcessor,但 BeanFactory 需要手动注册,ApplicationContext 则是自动注册。
49、什么是 JavaConfig?
JavaConfig 是 Spring3.0 新增的概念,就是以注解的形式取代 Spring 中繁琐的 xml 文件。
JavaConfig 结合了 xml 的解耦和 java 编译时检查的优点。
- @Configuration,表示这个类是配置类;
- @ComponentScan,相当于 xml 的 <context:componentScan basepackage=>;
- @Bean,相当于 xml 的 ;
- @EnableWebMvc,相当于 xml 的 mvc:annotation-driven;
- @ImportResource,相当于 xml 的 ;
- @PropertySource,用于读取 properties 配置文件;
- @Profile,一般用于多环境配置,激活时可用 @ActiveProfile(“dev”) 注解;
50、什么是 ORM 框架?
ORM(Object-relational mapping),对象关系映射。
是为了解决面向对象与关系型数据库存在的不匹配问题。
ORM 框架的优点:
- 开发效率更高
- 数据访问更抽象、轻便
- 支持面向对象封装