面试要点4

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

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

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

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

 ---ThreadLocal---

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

一、ThreadLocal不是线程,是线程的一个变量,你能够先简单理解为线程类的属性变量。mysql

  二、ThreadLocal在类中一般定义为静态变量。web

  三、每一个线程有本身的一个ThreadLocal,它是变量的一个“拷贝”,修改它不影响其余线程。spring

  既然定义为类变量,为什么为每一个线程维护一个副本(姑且称为“拷贝”容易理解),让每一个线程独立访问?多线程编程的经验告诉咱们,对于线程共享资源(你能够理解为属性),资源是否被全部线程共享,也就是说这个资源被一个线程修改是否影响另外一个线程的运行,若是影响咱们须要使用synchronized同步,让线程顺序访问。sql

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

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

 

1、Mysql的两种存储引擎编程

  一、MyISAM:bootstrap

    ①不支持事务,可是整个操做是原子性的(事务具有四种特性:原子性、一致性、隔离性、持久性)数组

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

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

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

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

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

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

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

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

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

    ⑦对于AUTO_INCREMENT类型的字段,在MyISAM表中,能够和其余字段一块儿创建联合索引。

        二、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

根据代码设计,咱们用图标来展现一下

 

 

 

一、RUNNING

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

二、SHUTDOWN

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

三、STOP

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

四、TIDYING

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

五、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();
  }
}
相关文章
相关标签/搜索