面试总结

面试总结

1、JAVA面向对象最重要的特征就是:封装,继承,多态。

(1)、封装:也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

好处:良好的封装可以降低耦合度;类的内部可以自由修改;类具有对外的清晰接口。

(2)、继承:是面向对象最显著的一个特性,是从已有的类中派生出新的类,我们把它称之为子类,子类继承父类的属性和行为,并能根据自己的需求扩展出新的属性和行为,提高了代码的可复用性

缺点:父类变,子类不得不变,父子是一种强耦合的关系。

(3)、多态:不同的对象可以执行相同的动作,但要通过他们自己的实现代码来执行。

同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。

有两种多态的机制:编译时多态、运行时多态

1、方法的重载:重载是指同一类中有多个同名的方法,但这-*

2、list与set方法的区

list与set方法的区别有:list可以允许重复对象和插入多个null值,而set不允许;list容器是有序的,而set容器是无序的等等

Java中的集合共包含三大类,它们分别是Set(集),List(列表)以及Map(映射)。它们都处在java.util中并且都为接口。它们各自都有各自的实现类。Set的实现类主要有HashSet和TreeSet,List的实现类主要有ArrayList。接下来将为大家介绍这两大类之间的区别,具有一定的参考作用,希望对大家有所帮助。

img

List方法与set方法的区别

(1)重复对象

list方法可以允许重复的对象,而set方法不允许重复对象

(2)null元素

list可以插入多个null元素,而set只允许插入一个null元素

(3)容器是否有序

list是一个有序的容器,保持了每个元素的插入顺序。即输出顺序就是输入顺序,而set方法是无序容器,无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序

(4)常用的实现类

list方法常用的实现类有ArrayList、LinkedList 和 Vector。其中ArrayList 最为流行,它提供了使用索引的随意访问,而LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适,Vector 表示底层数组,线程安全

Set方法中最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和compareTo() 的定义进行排序的有序容器

3、ArrayList和LinkedList的区别

ArrayList: 可以看作是能够自动增长容量的数组

ArrayList的toArray方法返回一个数组

ArrayList的asList方法返回一个列表

LinkList是一个双向链表,在添加和删除元素时具有比ArrayList更好的性能.但在get与set方面弱于ArrayList.当然,这些对比都是指数据量很大或者操作很频繁。

ArrayList和Vector使用了数组的实现,可以认为ArrayList或者Vector封装了对内部数组的操作,比如向数组中添加,删除,插入新的元素或者数据的扩展和重定向。

LinkedList使用了循环双向链表数据结构。与基于数组ArrayList相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。

LinkedList链表由一系列表项连接而成。一个表项总是包含3个部分:元素内容,前驱表和后驱表,如图所示:

img

在下图展示了一个包含3个元素的LinkedList的各个表项间的连接关系。在JDK的实现中,无论LikedList是否为空,链表内部都有一个header表项,它既表示链表的开始,也表示链表的结尾。表项header的后驱表项便是链表中第一个元素,表项header的前驱表项便是链表中最后一个元素。

、

4、HashMap

HashMap的总体结构如下:

在这里插入图片描述

HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系的一种集合)

HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

默认值

初始化大小:initialCapacity默认为16,

加载因子:loadFactory默认为0.75

HashMap的数组长度一定保持2的次幂,保证HashMap上数组上的元素根据Hash值均匀分布。

JDK1.8在JDK1.7的基础上针对增加了红黑树来进行优化。即当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

5、HashMap和CurrentHashMap的区别

HashMap线程不安全、数组+链表结构、JDK1.8之前数据+链表,之后数据+链表+红黑树。

CurrentHashMap线程安全、数组+链表结构、JDK1.8抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。

1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

6、CAS

CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

CAS的缺点:

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  1. 循环时间长开销很大。
  2. 只能保证一个变量的原子操作。
  3. ABA问题。

7、volatile的原理

volatile的作用:

  • 保持内存可见性
  • 防止指令重排
  • 但是不保证原子性
(1)保持内存可见性
public class VolatileTest {
    //num不进行volatile关键字修饰,在线程中就会一直死循环。当volatile修饰后,在线程中num就是可见性,就不会死循环
    private volatile static int num = 0;
    public static void main(String[] args) {
        System.out.println("main线程--start");
        new Thread(()->{
            System.out.println("A");
            while (num == 0){
                System.out.println(num);
            }
            System.out.println("B");
        }).start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num=1; 
        System.out.println("main线程--end");
    }
}

但是不保证原子性,保证原子性可以加锁:synchronized和lock

在不加synchronized和lock情况下可以使用原子类保证保证原子性。

public class VolatileTest {
    private volatile static int num = 0;
    private static void add(){
        num++;
    }
    public static void main(String[] args) {
        for (int i = 1; i <= 200; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    add();
                }
            }).start();
        }
        while(Thread.activeCount()>2){
            Thread.yield();         //程序运行两个线程:main 和gc,只要运行的线程数大于2,则还有线程没有跑完,就等待。。
        }
        System.out.println("执行完成-----"+num);
    }
}

1、多次执行完成结果如下:
执行完成-----198530
执行完成-----198795
执行完成-----199516
    
原因就是volatile不保证原子性,因为num++不是原子性操作,利用javap 查看字节码文件查看可知。
2、如果 add() 加上synchronized,那么结果就总会是:
执行完成-----200000
3、如果在不加synchronized和lock情况下可以使用原子类保证保证原子性。
修改代码如下:使用AtomicInteger原子类
public class VolatileTest {
    //private volatile static int num = 0;
    private volatile static  AtomicInteger num = new AtomicInteger();
    private  static void add(){
        //num++;
        num.getAndIncrement();               //调用底层Unsafe类,都是native方法
    }
    public static void main(String[] args) {
        for (int i = 1; i <= 200; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    add();
                }
            }).start();
        }
        while(Thread.activeCount()>2){
            Thread.yield();       //程序运行两个线程:main 和gc,只要运行的线程数大于2,则还有线程没有跑完,就等待。。
        }
        System.out.println("执行完成-----"+num);
    }
}

以上执行结果:
执行完成-----200000

8、悲观锁和乐观锁

(1)悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

(2)乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制加入一个字段version版本号,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

9、8锁现象

(1)多个线程使用同一把锁-顺序执行

(2)多个线程使用同一把锁,其中某个线程里面还有阻塞-顺序先执行

多个线程使用同一个对象,多个线程就是使用一把锁,先调用的先执行,即使在某方法中设置了阻塞。

(3)多个线程有锁与没锁-随机执行

多个线程,有的线程有锁,有的线程没锁,两者之间不存在竞争同一把锁的情况,先后执行顺序是随机的。

(4)多个线程使用多把锁-随机执行

1、被 synchronized 修饰的方法,锁的对象是方法的调用者;
2、调用者不同,它们之间用的不是同一个锁,相互之间没有关系。

(5)Class锁:多个线程使用一个对象-顺序执行

被 synchronized 和 static 同时修饰的方法,锁的对象是类的 class 对象,是唯一的一把锁。线程之间是顺序执行。

锁Class和锁对象的区别:

1、Class 锁 ,类模版,只有一个;

2、对象锁 , 通过类模板可以new 多个对象。

如果全部都锁了Class,那么这个类下的所有对象都具有同一把锁。

(6)Class锁:多个线程使用多个对象-顺序执行

被 synchronized 修饰 和 static 修饰的方法,锁的对象是类的 class 对象,是唯一的一把锁。

Class锁是唯一的,所以多个对象使用的也是同一个Class锁。

(7)Class锁与对象锁:多个线程使用一个对象-随机执行

被 synchronized和static修饰的方法,锁的对象是类的class对象!唯一的同一把锁;

只被synchronized修饰的方法,是普通锁(如对象锁),不是Class锁,所以进程之间执行顺序互不干扰。

(8)Class锁与对象锁:多个线程使用多个对象-随机执行

被 synchronized和static修饰的方法,锁的对象是类的class对象!唯一的同一把锁;

只被synchronized修饰的方法,是普通锁(如对象锁),不是Class锁,所以进程之间执行顺序互不干扰。

10、不可重入锁和重入锁的理解

  • 可重入锁:当线程获取某个锁后,还可以继续获取它,可以递归调用,而不会发生死锁;

  • 不可重入锁:与可重入相反,获取锁后不能重复获取,否则会死锁(自己锁自己)。

    可重入锁 synchronized和 ReentrantLock

11、分布式锁

分布式锁应该具备哪些条件:

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

分布式锁三种实现方式:

基于数据库实现分布式锁;唯一性约束
基于缓存(Redis等)实现分布式锁; 利用 SET NX 命令
基于Zookeeper实现分布式锁;Zookeeper分布式锁的原理,临时顺序节点

Zookeeper和Redis分布式锁的优缺点:

img

12、事务的ACID特性

事务具有4个特征,分别是原子性、一致性、隔离性和持久性,简称事务的ACID特性;

一、原子性(atomicity)

一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作,这就是事务的原子性

二、一致性(consistency)

事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。

如果数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所作的修改有一部分已写入物理数据库,这是数据库就处于一种不正确的状态,也就是不一致的状态

三、隔离性(isolation)

事务的隔离性是指在并发环境中,并发的事务时相互隔离的,一个事务的执行不能不被其他事务干扰。不同的事务并发操作相同的数据时,每个事务都有各自完成的数据空间,即一个事务内部的操作及使用的数据对其他并发事务时隔离的,并发执行的各个事务之间不能相互干扰。

在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同,分别是:未授权读取,授权读取,可重复读取和串行化

1、读未提交(Read Uncommited),该隔离级别允许脏读取,其隔离级别最低;比如事务A和事务B同时进行,事务A在整个执行阶段,会将某数据的值从1开始一直加到10,然后进行事务提交,此时,事务B能够看到这个数据项在事务A操作过程中的所有中间值(如1变成2,2变成3等),而对这一系列的中间值的读取就是未授权读取

2、授权读取也称为已提交读(Read Commited),授权读取只允许获取已经提交的数据。比如事务A和事务B同时进行,事务A进行+1操作,此时,事务B无法看到这个数据项在事务A操作过程中的所有中间值,只能看到最终的10。另外,如果说有一个事务C,和事务A进行非常类似的操作,只是事务C是将数据项从10加到20,此时事务B也同样可以读取到20,即授权读取允许不可重复读取。

3、可重复读(Repeatable Read)

就是保证在事务处理过程中,多次读取同一个数据时,其值都和事务开始时刻是一致的,因此该事务级别禁止不可重复读取和脏读取,但是有可能出现幻影数据。所谓幻影数据,就是指同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果。在上面的例子中,可重复读取隔离级别能够保证事务B在第一次事务操作过程中,始终对数据项读取到1,但是在下一次事务操作中,即使事务B(注意,事务名字虽然相同,但是指的是另一个事务操作)采用同样的查询方式,就可能读取到10或20;

4、串行化

是最严格的事务隔离级别,它要求所有事务被串行执行,即事务只能一个接一个的进行处理,不能并发执行。

四、持久性(durability)

一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中。--即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束的状态

13、事务@Transactional 注解问题

  • 场景一:serviceA 方法调用了 serviceB 方法,但两个方法都有事务,这个时候如果 serviceB 方法异常,是让 serviceB 方法提交,还是两个一起回滚。
  • 场景二:serviceA 方法调用了 serviceB 方法,但是只有 serviceA 方法加了事务,是否把 serviceB 也加入 serviceA 的事务,如果 serviceB 异常,是否回滚 serviceA 。
  • 场景三:serviceA 方法调用了 serviceB 方法,两者都有事务,serviceB 已经正常执行完,但 serviceA 异常,是否需要回滚 serviceB 的数据。

并不是通过代理类去调用,而是通过this调用本身的方法。导致事务不生效。

两种处理方式:

  1. 将 serviceB写入到另外的类中,然后引入实例调用。这样事务就会生效了.

  2. 第2种方式通过AopContext创建一个代理

14、创建线程的三种方式

(1)、继承Thread方法,重写Thread的run()方法

MyThread myThread=new MyThread();
myThread.start();//开启一个线程方法

(2)、是实现Runnable接口,实现run()方法。

Runnable runnable=new RunnableThread();
Thread thread=new Thread(runnable);
thread.start();//开启一个线程方法

(3)、是实现Callable接口和Future创建线程实现call()方法,并且有返回值

Callable<String> callable=new CallableThread();
FutureTask<String> futureTask=new FutureTask<String>(callable);
Thread thread=new Thread(futureTask);
thread.start();//开启一个线程方法

15、创建线程池有哪几种方式?

四个方式创建线程:

①. newFixedThreadPool(int nThreads)

创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。

②. newCachedThreadPool()

创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。

③. newSingleThreadExecutor()

这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。

④. newScheduledThreadPool(int corePoolSize)

创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

创建线程池的7个参数:

ThreadPoolExecutor(int corePoolSize,   //核心线程数                      
                   int maximumPoolSize, //最大线程数
                   long keepAliveTime,// 超时时间 多久释放线程资源
                   TimeUnit unit,       // 超时时间单位
                   BlockingQueue<Runnable> workQueue, // 工作队列。阻塞队列
                   ThreadFactory threadFactory, //线程工厂 
                   RejectedExecutionHandler handler) //拒绝策略

四种工作队列:

直接提交队列:
SynchronousQueue,SynchronousQueue是没有容量的容器,每一个插入的操作都需要等待相应的删除操作,SynchronousQueue不保存任务,它总是马上将任务提交给线程执行,如果没有空闲的线程则会尝试创建新的线程,如果线程数量已经达到最大值,则执行拒绝策略,使用SynchronousQueue通常需要设置很大的maximumPoolSize
有界的任务队列:
有界队列可以使用ArrayBlockingQueue,ArrayBlockingQueue的构造函数必须传入一个容量参数,表示队列的最大容量,当使用有界队列并有新任务时,若然线程池线程数量小于corePoolSize则会创建现场,若然大于corePoolSize则会将新任务加入任务队列,当任务队列已满无法加入时,则在总线程数不大于maximumPoolSize的前提下创建线程,若大于maximumPoolSize则执行拒绝策略,使用有界队列除非系统非常繁忙,否则确保核心线程数在corePoolSize
无界的任务队列:
无界任务队列可以使用LinkedBlockingQueue,与有界队列相比,除非系统资源耗尽,否则不会存在任务入队失败的情况.若任务创建和处理速度差异很大,无界队列会快速膨胀导致系统资源耗尽

优先任务队列:
优先任务队列使用PriorityBlockingQueue实现,PriorityBlockingQueue是一个特殊的无界队列,创建PriorityBlockingQueue时可以传入Comparator对任务进行优先级处理,PriorityBlockingQueue和无界队列可能会发生的问题一样,不过PriorityBlockingQueue能控制任务的优先级别

四种拒绝策略:

AbortPolicy: ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。(默认)

DiscardPolicy:ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。

DiscardOldestPolicy:ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

CallerRunsPolicy:ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

最大线程数的确定:

Nthreads=Ncpu*(1+w/c)

IO密集型:一般情况下,如果存在IO,那么肯定w/c>1(阻塞耗时一般都是计算耗时的很多倍),但是需要考虑系统内存有限(每开启一个线程都需要内存空间),这里需要上服务器测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。如果不想去测试,保守点取1即,Nthreads=Ncpu*(1+1)=2Ncpu。这样设置一般都OK。

计算密集型:假设没有等待w=0,则W/C=0. Nthreads=Ncpu。

16、Java中的代理模式

代理模式:就是为其他对象提供一种代理以控制对这个对象的访问。

代理可以在不改动目标对象的基础上,增加其他额外的功能(扩展功能)。

(1)静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象(目标对象)与代理对象(Proxy)一起实现相同的接口或者是继承相同父类。编译时已经写好了代理类。

静态代理总结:

可以实现在不修改目标对象的基础上,对目标对象的功能进行扩展。

但是由于代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护.

可以使用动态代理方式来解决。

(2)动态代理(JDK代理)

*动态代理有以下特点:*

1.代理对象,不需要实现接口
2.代理对象的生成,是利用JDK的API,动态的在内存中创建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
3.动态代理也叫做:JDK代理,接口代理

JDK动态代理实现过程:

  1. 通过getProxyClass0()生成代理类。JDK生成的最终真正的代理类,它继承自Proxy并实现了我们定义的接口.

  2. 通过Proxy.newProxyInstance()生成代理类的实例对象,创建对象时传入InvocationHandler类型的实例。

  3. 调用新实例的方法,即此例中的save(),即原InvocationHandler类中的invoke()方法。

代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理

(3)Cglib代理

JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。

Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展

Cglib子类代理实现方法:
1.需要引入cglib的jar文件,但是Spring的核心包中已经包括了Cglib功能,所以直接引入Spring-core.jar即可.
2.引入功能包后,就可以在内存中动态构建子类
3.代理的类不能为final,否则报错
4.目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.

17、拦截器和过滤器的区别

1.过滤器:

依赖于servlet容器。在实现上基于函数回调,可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的是用来做一些过滤操作,获取我们想要获取的数据,比如:在过滤器中修改字符编码;在过滤器中修改HttpServletRequest的一些参数,包括:过滤低俗文字、危险字符等

2.拦截器:

依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上基于Java的反射机制,属于面向切面编程(AOP)的一种运用。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理

3.过滤器和拦截器的区别:

①拦截器是基于java的反射机制的,而过滤器是基于函数回调。

②拦截器不依赖与servlet容器,过滤器依赖与servlet容器。

③拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。

④拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。

⑤在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。

⑥拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。

Filter和Interceptor的执行顺序:过滤前-拦截前-action执行-拦截后-过滤后

posted @ 2020-11-16 01:36  zxone公子  阅读(16)  评论(0编辑  收藏  举报