面试要点4

---java的类加载机制的原理?---

类加载是一个将类合并到正在运行着的JVM进程中的过程。首先要加载一个类,我们必须先得将类文件加载进来并连接,并且要加上大量的验证,随后会生成一个代表着这个类的class对象,然后就可以通过它去创建新的实例了。
    这就是我所理解的Java的类加载机制。
    经过加载和连接后出来的class对象,说明这个类已经被加载到了JVM中,此后就不会再加载了。

 ---分布式session共享怎么设计?---

一、Session Replication 方式管理 (即session复制)
简介:将一台机器上的Session数据广播复制到集群中其余机器上
使用场景:机器较少,网络流量较小
优点:实现简单、配置较少、当网络中有机器Down掉时不影响用户访问
缺点:广播式复制到其余机器有一定廷时,带来一定网络开销
二、Session Sticky 方式管理
简介:即粘性Session、当用户访问集群中某台机器后,强制指定后续所有请求均落到此机器上
使用场景:机器数适中、对稳定性要求不是非常苛刻
优点:实现简单、配置方便、没有额外网络开销
缺点:网络中有机器Down掉时、用户Session会丢失、容易造成单点故障
三、缓存集中式管理
简介:将Session存入分布式缓存集群中的某台机器上,当用户访问不同节点时先从缓存中拿Session信息
使用场景:集群中机器数多、网络环境复杂
优点:可靠性好
缺点:实现复杂、稳定性依赖于缓存的稳定性、Session信息放入缓存时要有合理的策略写入

 ---ThreadLocal---

ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。

1、ThreadLocal不是线程,是线程的一个变量,你可以先简单理解为线程类的属性变量。

  2、ThreadLocal在类中通常定义为静态变量。

  3、每个线程有自己的一个ThreadLocal,它是变量的一个“拷贝”,修改它不影响其他线程。

  既然定义为类变量,为何为每个线程维护一个副本(姑且称为“拷贝”容易理解),让每个线程独立访问?多线程编程的经验告诉我们,对于线程共享资源(你可以理解为属性),资源是否被所有线程共享,也就是说这个资源被一个线程修改是否影响另一个线程的运行,如果影响我们需要使用synchronized同步,让线程顺序访问。

  ThreadLocal适用于资源共享但不需要维护状态的情况,也就是一个线程对资源的修改,不影响另一个线程的运行;这种设计是‘空间换时间’,synchronized顺序执行是‘时间换取空间’。

---Mysql的两种存储引擎以及区别---

 

一、Mysql的两种存储引擎

  1、MyISAM:

    ①不支持事务,但是整个操作是原子性的(事务具备四种特性:原子性、一致性、隔离性、持久性)

    ②不支持外键,支持表锁,每次所住的是整张表

        MyISAM的表锁有读锁和写锁(两个锁都是表级别):

      表共享读锁和表独占写锁。在对MyISAM表进行读操作时,不会阻塞其他用户对同一张表的读请求,但是会阻塞其他用户对表的写请求;对其进行写操作时会阻塞对同一表读操作和写操作

      MyISAM存储引擎的读锁和写锁是互斥的,读写操作是串行的。那么,一个进程请求某个MyISAM表的读锁,同时另一个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!这是因为MySQL认为写请求一般比读请求要重要。这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。这种情况有时可能会变得非常糟糕! 

    ③一个MyISAM表有三个文件:索引文件,表结构文件,数据文件

    ④存储表的总行数,执行select count(*) from table时只要简单的读出保存好的行数即可

      (myisam存储引擎的表,count(*)速度快的也仅仅是不带where条件的count。这个想想容易理解的,因为你带了where限制条件,原来所以中缓存的表总数能够直接返回用吗?不能用。这个查询引擎也是需要根据where条件去表中扫描数据,进行统计返回的。)

    ⑤采用非聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。

    ⑥支持全文索引和空间索引

    ⑦对于AUTO_INCREMENT类型的字段,在MyISAM表中,可以和其他字段一起建立联合索引。

        2、Innodb:

    ①支持事务,支持事务的四种隔离级别;是一种具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。

    ②支持行锁和外键约束,因此可以支持写并发

    ③不存储总行数;也就是说,执行select count(*) from table时,InnoDB要扫描一遍整个表来计算有多少行。注意的是,当count(*)语句包含 where条件时,两种表的操作是一样的。

    ④对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引

    ⑤DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除

    ⑥一个Innodb表存储在一个文件内(共享表空间,表大小不受操作系统的限制),也可能为多个(设置为独立表空间,表大小受操作系统限制,大小为2G),受操作系统文件大小的限制

    ⑦主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问主键索引;最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。

---Spring Boot 的核心配置文件有哪几个?它们的区别是什么?---

Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。

application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。

bootstrap 配置文件有以下几个应用场景。

  • 使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
  • 一些固定的不能被覆盖的属性;
  • 一些加密/解密的场景;

---Spring Boot 的配置文件有哪几种格式?它们有什么区别?---

.properties 和 .yml,它们的区别主要是书写格式不同。

1).properties : 例 app.user.name = javastack

2).yml : 例 app:

        user:

                            name: javastack

另外,.yml 格式不支持 @PropertySource 注解导入配置。

---Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?---

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。

@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

@ComponentScan:Spring组件扫描。

---运行 Spring Boot 有哪几种方式?---

1)打包用命令或者放到容器中运行

2)用 Maven/ Gradle 插件运行

3)直接执行 main 方法运行

---Spring Boot 可以兼容老 Spring 项目吗,如何做?---

可以兼容,使用 @ImportResource 注解导入老 Spring 项目配置文件。

---你如何理解 Spring Boot 中的 Starters?---

Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。

Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。

 ---spring注解事务Transactional自调用失效---

 如果一个类中自身方法的调用,我们称之为自调用。如一个订单业务实现类OrderServiceImpl中有methodA方法调用了自身类的methodB方法就是自调用,如:

如果一个类中自身方法的调用,我们称之为自调用。如一个订单业务实现类OrderServiceImpl中有methodA方法调用了自身类的methodB方法就是自调用,如:

@Transactional
public void methodA(){
    for (int i = 0; i < 10; i++) {
        methodB();
    }
}
    
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
public int methodB(){
    ......
}

在上面方法中不管methodB如何设置隔离级别和传播行为都是不生效的。即自调用失效。

这主要是由于@Transactional的底层实现原理是基于AOP实现,而AOP的原理是动态代理,在自调用的过程中是类自身的调用,而不是代理对象去调用,那么就不会产生AOP,于是就发生了自调用失败的现象。

要克服这个问题,有2种方法:

  • 编写两个Service,用一个Service的methodA去调用另外一个Service的methodB方法,这样就是代理对象的调用,不会有问题;
  • 在同一个Service中,methodA不直接调用methodB,而是先从Spring IOC容器中重新获取代理对象`OrderServiceImpl·,获取到后再去调用methodB。说起来有点乱,还是show you the code。
public class OrderServiceImpl implements OrderService,ApplicationContextAware {
    private ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Transactional
    public void methodA(){
        OrderService orderService = applicationContext.getBean(OrderService.class);
        for (int i = 0; i < 10; i++) {
            orderService.methodB();
        }
    }

    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
    public int methodB(){
        ......
    }

}

上面代码中我们先实现了ApplicationContextAware接口,然后通过applicationContext.getBean()获取了OrderService的接口对象。这个时候获取到的是一个代理对象,也就能正常使用AOP的动态代理了。

 ---spring对于有状态bean的并发问题解决方案---

两种解决方案:

1.将有状态的bean配置成prototype模式,让每一个线程都创建一个prototype实例。但是这样会产生很多的实例消耗较多的内存空间。

2.使用ThreadLocal变量,为每一条线程设置变量副本。(典型应用就是从数据库连接池中获取connection对象,同一个线程共享,不同线程隔离)

 

 ---mysql的时间字段问题---

MySQL中用来表示时间的字段类型有:DATE、DATETIME、TIMESTAMP,它们之间有相同点,各自也有自己的特性,我总结了一个表格,如下所示:image.png

在开发中,应该尽量避免使用时间戳作为查询条件(如:selelct * from user where modified_time >= #{date}),如果必须要用,则需要充分考虑MySQL的精度和查询参数的精度等问题。例如:mysql-connector-java在5.1.23之前会将秒后面的精度丢弃再传给MySQL服务端,正好我们使用的mysql版本中DATETIME的精度是秒;在我将mysql-connector-java升级到5.1.30后,从java应用通过mysql-connector-java将时间戳传到MySQL服务端的时候,就不会将毫秒数丢弃了,从mysql-connector-java的角度看是修复了一个BUG,但是对于我们的应用来说却可能触发了一个BUG。

 --- javaweb filter---

Filter是一个实现了javax.servlet.Filter接口的类。

Filter接口中的方法:

  • init(FilterConfig  filterFonfig)    //初始化Filter
  • doFilter(ServletRequest request, ServletResponse response, FilterChain  chain)     //拦截、过滤。chain对象表示Filter链。此方法是Filter的关键方法。
  • destroy()   //在web服务器移除Filter对象之前调用,释放Filter对象占用的资源
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        //......    //去的时候拦截,做一些处理
        chain.doFilter(req, resp);  //放行
        //......    //回来的时候拦截,做一些处理
    }

 Filter接口两种配置方式:

(1)web.xml中配置

<filter>
        <filter-name>handlerFilter</filter-name>
        <filter-class>filter.HandlerFilter</filter-class>
        <init-param>
            <param-name>name</param-name>
            <param-value>张三</param-value>
        </init-param>
        <init-param>
            <param-name>age</param-name>
            <param-value>20</param-value>
        </init-param>
    </filter>

如果有多个Filter同时拦截某个请求,这些Filter会组成一个FilterChain,拦截时,在xml配置中位置靠前的Filter-mapping先拦截。

(2)注解配置

@WebFilter(
        filterName = "handlerFilter",
        urlPatterns = "/*",
        initParams = {@WebInitParam(name = "name", value = "张三"),@WebInitParam(name="age",value = "12")}
        )

Filter接口使用示例:

public class HandlerFilter implements Filter {
private FilterConfig filterConfig; //需要创建一个成员变量
public void init(FilterConfig config){
this.filterConfig = config; //需要我们手动初始化
}

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
//获取单个初始参数的值
String name = filterConfig.getInitParameter("name"); //返回值是String,不存在该参数时返回null
String age = filterConfig.getInitParameter("age");
System.out.println(name);
System.out.println(age);

//遍历
Enumeration<String> initParameterNames = filterConfig.getInitParameterNames();
while (initParameterNames.hasMoreElements()){
String paramName = initParameterNames.nextElement();
String paramValue = filterConfig.getInitParameter(paramName);
System.out.println(paramValue);
}

chain.doFilter(req, resp);
}

public void destroy() {
}

}

 ---Executor线程池---

引用自《阿里巴巴JAVA开发手册》

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

 

Executor接口是线程池框架中最基础的部分,定义了一个用于执行Runnable的execute方法。Exectuor下有一个重要的子接口ExecutorService,其中定义了线程池的具体行为

    • execute(Runnable runnable):执行Runnable类型的任务
    • submit(task):用来提交Callable或者Runnable任务,并返回代表此任务的Future对象
    • shutdown():在完成已经提交的任务后封闭办事,不在接管新的任务
    • shutdownNow():停止所有正在履行的任务并封闭办事
    • isTerminated():是一个钩子函数,测试是否所有任务都履行完毕了
    • isShutdown():是一个钩子函数,测试是否该ExecutorService是否被关闭

线程池的状态

线程池使用了一个Integer类型变量来记录线程池任务数量和线程池状态信息,很巧妙。这个变量ctl,被定义为了AtomicInteger,使用高3位来表示线程池状态低29位来表示线程池中的任务数量

RUNNING = ­1 << COUNT_BITS; //高3位为111
SHUTDOWN = 0 << COUNT_BITS; //高3位为000
STOP = 1 << COUNT_BITS; //高3位为001
TIDYING = 2 << COUNT_BITS; //高3位为010
TERMINATED = 3 << COUNT_BITS; //高3位为011

根据代码设计,我们用图标来展示一下

 

 

 

1、RUNNING

  • 状态说明:线程池处于RUNNING状态,能够接收新任务,以及对已添加的任务进行处理。
  • 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。

2、SHUTDOWN

  • 状态说明:线程池处于SHUTDOWN状态,不接收新任务,能够处理已经添加的任务。
  • 状态切换:调用shutdown()方法时,线程池由RUNNING -> SHUTDOWN。

3、STOP

  • 状态说明:线程池处于STOP状态,不接收新任务,不处理已提交的任务,并且会中断正在处理的任务。
  • 状态切换:调用线程池中的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN) -> STOP。

4、TIDYING

  • 状态说明:当所有的任务已经停止,ctl记录“任务数量”为0,线程池会变为TIDYING状态。当线程池处于TIDYING状态时,会执行钩子函数 terminated()。 terminated()在ThreadPoolExecutor类中是空, 的,若用户想在线程池变为TIDYING时,进行相应处理,可以通过重载 terminated()函数来实现。
  • 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行任务也为空时,就会由SHUTDOWN -> TIDYING。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP-> TIDYING。

5、TERMINATED

  • 状态说明:线程池线程池彻底停止,线程池处于TERMINATED状态,
  • 状态切换:线程池处于TIDYING状态时,执行完terminated()之后, 就会由TIDYING->TERMINATED。
线程池的创建
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
{ this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); }
  • corePoolSize:线程池中的核心线程数。当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。除非设置了allowCoreThreadTimeOut,否则核心线程将持续保留在线程池中即时没有新的任务提交过来。
  • maximumPoolSize:线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize。
  • keepAliveTime:线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize时候,如果这时候没有新的任务提交,核心线程外的线程不会立即被销毁,而是会等待,直到等待的时间超过了keepAliveTime
    unit:keepAliveTime的单位时间
  • workQueue:用于保存等待被执行的任务的阻塞队列,且任务必须实现Runnable接口,在JDK中提供了如下阻塞队列:
    ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务。
    LinkedBlockingQueue:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQueue。
    SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常高于LinkedBlockingQueue。
  • PriorityBlockingQueue:具有优先级的无界阻塞队列。
  • threadFactory:ThreadFactory 类型的变量,用来创建新线程。默认使用ThreadFactory.defaultThreadFactory来创建线程, 会使新创建线程具有相同的NORM_PRIORITY优先级并且都是非守护线程,同时也设置了线程名称。
  • handler:线程池的饱和策略。当阻塞队列满了,且没有空闲的工作队列,如果继续提交任务,必须采用一种策略处理该任务.

我们简化下上面的文字,用简单表格来展示:

我们再用流程图来对线程池中提交任务的这一逻辑增加感性认识:

线程池的监控

public long getTaskCount() //线程池已执行与未执行的任务总数
public long getCompletedTaskCount() //已完成的任务数
public int getPoolSize() //线程池当前的线程数
public int getActiveCount() //线程池中正在执行任务的线程数量

JDK提供的常用的线程池

一般情况下我们都不直接用ThreadPoolExecutor类来创建线程池,而是通过Executors工具类去构建,通过Executors工具类我们可以构造5种不同的线程池。

newFixedThreadPool(int nThreads):

创建固定线程数的线程池,corePoolSize和maximumPoolSize是相等的,默认情况下,线程池中的空闲线程不会被回收的;

newCachedThreadPool:

创建线程数量不定的线程池,线程数量随任务量变动,一旦来了新的任务,如果线程池中没有空闲线程则立马创建新的线程来执行任务,空闲线程存活时间60秒,过后就被回收了,可见这个线程池弹性很高;

newSingleThreadExecutor:

创建线程数量为1的线程池,等价于newFixedThreadPool(1)所构造的线程池;

newScheduledThreadPool(int corePoolSize):

创建核心线程数为corePoolSize,可执行定时任务的线程池;

newSingleThreadScheduledExecutor:

等价于newScheduledThreadPool(1)。

阻塞队列

构造函数中的队列允许我们自定义,队列的意义在于缓存无法得到线程执行的任务,当线程数量大于corePoolSize而当前workQueue还没有满时,就需要将任务放置到队列中。JDK提供了几种类型的队列容器,每种类型都具各自特点,可以根据实际场景和需要自行配置到线程池中。

ArrayBlockingQueue:

有界队列,基于数组结构,按照队列FIFO原则对元素排序;

LinkedBlockingQueue:

无界队列,基于链表结构,按照队列FIFO原则对元素排序,Executors.newFixedThreadPool()使用了这个队列;

SynchronousQueue:

同步队列,该队列不存储元素,每个插入操作必须等待另一个线程调用移除操作,否则插入操作会一直被阻塞,Executors.newCachedThreadPool()使用了这个队列;

PriorityBlockingQueue:

优先级队列,具有优先级的无限阻塞队列。

拒绝策略

拒绝策略(RejectedExecutionHandler)也称饱和策略,当线程数量和队列都达到饱和时,就采用饱和策略来处理新提交过来的任务,默认情况下采用的策略是抛出异常(AbortPolicy),表示无法处理直接抛出异常,其实JDK提供了四种策略,也很好记,拒绝策略无非就是抛异常、执行或者丢弃任务,其中丢弃任务就分为丢弃自己或者丢弃队列中最老的任务,下面简要说明一下:

AbortPolicy:丢弃新任务,并抛出 RejectedExecutionException

DiscardPolicy:不做任何操作,直接丢弃新任务

DiscardOldestPolicy:丢弃队列队首(最老)的元素,并执行新任务

CallerRunsPolicy:由当前调用线程来执行新任务

使用技巧

使用了线程池技术未必能够给工作带来利好,在没能正确理解线程池特性以及了解自身业务场景下而配置的线程池,可能会成为系统性能或者业务的瓶颈甚至是漏洞,所以在我们使用线程池时,除了对线程池本身特性了如指掌,还需要对自身业务属性进行一番分析,以便配置出合理的高效的线程池以供项目使用,下面我们从这几个方面来分析:

  • 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  • 任务的优先级:高,中和低。
  • 任务的执行时间:长,中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池,以减少线程切换带来的性能开销。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

当然,以上这些配置方式都是经验值,实际当中还需要分析自己的项目场景经过多次测试方可得出最适合自己项目的线程池配置。

 

 
 

 ---事务的ACID属性之间的关系---

这几个特性不是一种平级关系:

  • 只有满足一致性,事务的执行结果才是正确的。
  • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时要只要能满足原子性,就一定能满足一致性。
  • 在并发的情况下,多个事务并发执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
  • 事务满足持久化是为了能应对数据库奔溃的情况。

---事务的隔离级别---

MYSQL常看当前数据库的事务隔离级别:show variables like 'tx_isolation';

 

---Springmvc的拦截器执行顺序及各方法作用---

 

实现HandlerInterceptor接口或者继承HandlerInterceptor的子类,比如Spring 已经提供的实现了HandlerInterceptor 接口的抽象类HandlerInterceptorAdapter ,下面讲实现其接口的写法,先看一下这个接口的三个方法. 


方法preHandle: 顾名思义,该方法将在请求处理之前进行调用,在controller之前执行。SpringMVC 中的Interceptor 是链式的调用的,在一个应用中或者说是在一个请求中可以同时存在多个Interceptor 。每个Interceptor 的调用会依据它的声明顺序依次执行,而且最先执行的都是Interceptor 中的preHandle 方法,所以可以在这个方法中进行一些前置初始化操作或者是对当前请求的一个预处理,比如说获取cookie的值或者判断是否已经登录,也可以在这个方法中进行一些判断来决定请求是否要继续进行下去。该方法的返回值是布尔值Boolean 类型的,当它返回为false 时,表示请求结束,后续的Interceptor 和Controller 都不会再执行;当返回值为true 时就会继续调用下一个Interceptor 的preHandle 方法,如果已经是最后一个Interceptor 的时候就会是调用当前请求的Controller 方法。 


方法postHandle:由preHandle 方法的解释我们知道这个方法包括后面要说到的afterCompletion 方法都只能是在当前所属的Interceptor 的preHandle 方法的返回值为true 时才能被调用。postHandle 方法,顾名思义就是在当前请求进行处理之后,也就是Controller 方法调用之后执行,但是它会在DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作,比如说设置cookie,返回给前端。postHandle 方法被调用的方向跟preHandle 是相反的,也就是说先声明的Interceptor 的postHandle 方法反而会后执行


方法afterCompletion:该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。这个方法的主要作用是用于进行资源清理工作的。

 

例:

@Component
public class AuthInterceptor implements HandlerInterceptor {
  
  private static final String TOKEN_COOKIE = "token";
  
  
  @Autowired
  private UserDao userDao;

  
  @Override
  public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler)
          throws Exception {
    Map<String, String[]> map = req.getParameterMap();
    map.forEach((k,v) ->req.setAttribute(k, Joiner.on(",").join(v)));
    String requestURI = req.getRequestURI();
    if (requestURI.startsWith("/static") || requestURI.startsWith("/error")) {
      return true;
    }
    Cookie cookie = WebUtils.getCookie(req, TOKEN_COOKIE);
    if (cookie != null && StringUtils.isNoneBlank(cookie.getValue())) {
        User user = userDao.getUserByToken(cookie.getValue());
        if (user != null) {
          req.setAttribute(CommonConstants.LOGIN_USER_ATTRIBUTE, user);

          UserContext.setUser(user);
        }
    }
    return true;
  }
  

  @Override
  public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler,
          ModelAndView modelAndView) throws Exception {
    String requestURI = req.getRequestURI();
    if (requestURI.startsWith("/static") || requestURI.startsWith("/error")) {
      return ;
    }
    User user = UserContext.getUser();
    if (user != null && StringUtils.isNoneBlank(user.getToken())) {
       String token = requestURI.startsWith("logout")? "" : user.getToken();
       Cookie cookie = new Cookie(TOKEN_COOKIE, token);
       cookie.setPath("/");
       cookie.setHttpOnly(false);
       res.addCookie(cookie);
    }
    
  }

  @Override
  public void afterCompletion(HttpServletRequest req, HttpServletResponse response, Object handler, Exception ex)
          throws Exception {
    UserContext.remove();
  }
}
posted @ 2016-12-14 16:28  民间小农  阅读(120)  评论(0)    收藏  举报