Loading...
墨滴

jrh

2021/10/30  阅读:28  主题:橙心

Java 面试八股文之框架篇(三)

前言

这是系列文章【 Java 面试八股文】框架篇的第三期。

【 Java 面试八股文】系列会陆续更新 Java 面试中的高频问题,旨在从问题出发,带你理解 Java 基础,数据结构与算法,数据库,常用框架等。该系列前几期文章可以通过下方给出的链接进行查看~

按照惯例——首先要做几点说明:

  1. 【 Java 面试八股文】中的面试题来源于社区论坛,书籍等资源;感谢使我读到这些宝贵面经的作者们。
  2. 对于【 Java 面试八股文】中的每个问题,我都会尽可能地写出我自己认为的“完美解答”。但是毕竟我的身份不是一个“真理持有者”,只是一个秉承着开源分享精神的 “knowledge transmitter” & 菜鸡,所以,如果这些答案出现了错误,可以留言写出你认为更好的解答,并指正我。非常感谢您的分享。
  3. 知识在于“融释贯通”,而非“死记硬背”;现在市面上固然有很多类似于“Java 面试必考 300 题” 这类的文章,但是普遍上都是糟粕,仅讲述其果,而不追其源;希望我的【 Java 面试八股文】可以让你知其然,且知其所以然~

那么,废话不多说,我们正式开始吧!

往期文章

框架篇(二)

1、Spring MVC 的工作流程?


首先,简单谈一下什么是 Spring MVC。

Spring MVC 是一种在 Spring 基础上扩展出的应用于 Web 端的框架。从名字上就可以看出,Spring MVC 使用了三层架构模型(Model-View-Controller),把复杂的 Web 处理流程进行职责解耦,来达到简化开发的目的。

Spring MVC 框架中的重要组件有:

  1. DispatcherServlet
  2. HandlerMapping
  3. HandlerAdapter
  4. Handler
  5. ViewResolver
  6. View

接下来,我们就看一下这些组件的功能以及 Spring MVC 完整的工作流程:

Spring MVC 中最重要的组件就是 DispatcherServlet,它用来处理所有的 HTTP 请求和响应以及负责各个组件的职责调度,其核心方法为 DispatcherServlet#doDispatch(HttpServletRequest request, HttpServletResponse response)

HandlerMapping 为处理器映射器,其功能为根据 URL 找到对应的 Handler。在 DispatcherServlet#getHandler(HttpServletRequest request) 方法中会遍历定义好的 handlerMappings,找到并返回一个 HandlerExecutionChain 对象。HandlerExecutionChain 是对 Handler 的一个二次封装,不仅包含 Handler 对象,还包含着这个 Handler 的拦截器。

HandlerAdapter 为处理器适配器,其功能为执行 Handler。在 DispatcherServlet#getHandlerAdapter(mappedHandler.getHandler()) 方法中,会遍历注册好的 handlerAdapters,找到支持此 Handler 的 HandlerAdapter,然后调用 HandlerAdapter#handle 方法,返回一个 ModelAndView 对象。在执行这一步的前后,会调用 HandlerInterceptor 的 preHandle 方法与 postHandle 方法。

Handler 为处理器,其本质为对 Controller 的 Bean 本身以及请求方式包装的一个 Object 对象,需要我们开发工程师自己编写。

ViewResolver 为视图解析器。当 DispatcherServlet 拿到了 ModelAndView 对象后,会交给视图解析器进行解析。ModelAndView 对象中包含着 ModelMap 以及逻辑视图(一般就是 viewName),ViewResolver 会将逻辑视图解析为具体的 View 对象并返回给 DispatcherServlet。

而 View 本身则是一个接口,其作用为渲染数据,将数据以不同的形式如 JSP 呈现给用户。在我们在拿到了 View 对象后,会调用该对象的 render 方法,将模型数据填充到视图中,完成对视图的渲染,最后将渲染后的视图返回并呈现给用户。

整个流程如图所示:

2、Cookie 与 Session 相关


在 Spring MVC 中如何设置并获取 Cookie 与 Session 呢?

Cookie 由服务端创建后返回给客户端,并使这一小块数据保存在浏览器上;当我们的客户端再次访问服务器时,便会自动携带 Cookie。Cookie 存在的目的就是为了解决 HTTP 协议无状态的问题,创建并维持有状态的会话:

在 Spring MVC 框架中,创建 Cookie 与获取 Cookie 的代码如下所示:

public void addCookie(HttpServletResponse response){
    Cookie cookie = new Cookie("test""test");
    cookie.setPath("/");
    cookie.setMaxAge(5 * 60); // 5 min
    response.addCookie(cookie);
    // ... ...
}

public void getCookie(HttpServletRequest request){
    Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("test"))
                System.out.println("get Cookie");
            }
        }
    }
    // ... ...
}

我们对一个 Cookie 可以设置它的所在域(setPath),以及它的存活时间(setMaxAge)。也可以在方法传参中,使用 @CookieValue("cookieName") 注解来直接获取到 Cookie。

而 Session 则是一种依赖于 Cookie 实现的技术。因为 Cookie 保存在客户端,就难免会带来安全问题。为了避免这一点,Session 将会话保存在服务端。

使用 Session 的服务端与客户端交互示意图如下所示:

首先,服务端将 SessionId 通过 Cookie 的形式发送给客户端;当客户端访问服务端时,会携带着 SessionId,服务端通过读取 SessionId 进行验证,使得用户可以获取到 Session 的信息。

在 Spring MVC 框架中,创建 Session 与获取 Session 的代码如下所示:

public void addSession(HttpServletRequest request){
    HttpSession session = request.getSession(true);
    session.setAttribute("count"100);
    session.setMaxInactiveInterval(5 * 60); // 5 min
    // ... ...
}
public void getSession(HttpSession session){
    Object count = session.getAttribute("count");
    System.out.println(count);
    // ... ...
}

以上内容便是 Spring MVC 中设置并获取 Cookie 与 Session 的答案了。不过,本题既然涉及到了 Cookie 与 Session,面试官自然而然地就会问到 Cookie 与 Session 的相关知识点,我总结了以下两个常考的面试题:

  1. Cookie 与 Session 的区别?
  2. 如何解决分布式 Session?

面试题一:Cookie 与 Session 的区别?

  1. Cookie 数据存放在客户端,Session 数据存放在服务端

    Cookie 因为存放在客户端,且对客户端可见,所以安全性不如存放在服务端的 Session;而 Session 存放在服务端,它的缺点是会增加服务端的内存压力。在 Cookie 与 Session 的使用场景中,对于涉及到用户私人信息的数据我们应该使用 Session 进行保存,其他的绝大部分场景,譬如自动登录功能,则应该使用 Cookie 来解决。面试官可能会问:你不是说 Cookie 不安全么?那自动登录怎么用 Cookie 来实现?Cookie 中肯定不能存放用户密码这类的信息,一般情况下我的做法是生成一个随机的字符串 ticket,然后将其存放在 Redis 中,并设置其过期时间。每次用户重新登录或者修改密码的话都重新生成一个 ticket。我们使用 ticket 作为 Cookie,浏览器每次向服务器发起请求时,只要 Cookie 没有失效,就可以实现自动登录的功能,并且 Cookie 中也不包含用户名以及密码相关的内容。同时,为了防止 XSS 跨站脚本攻击,可以在 Cookie 中设置 Http-Only 属性为 True,也能保证一部分的安全。

  2. Cookie 与 Session 的存取方式不同

    Cookie 中只能保存 ASCII 码字符串,且保存的数据不能超过 4 KB,不能存取 Java 对象,所以如果要存储比较复杂的信息,那么使用 Cookie 是比较艰难的;而 Session 则可以存取任何类型的数据,包括而不限于 String,List,Map 等,运用起来比较方便。

  3. Cookie 与 Session 的生命周期不同

    当我们创建了一个 Cookie,设置了它的生命周期为 5 min ,那么从计时开始,5 min 结束后,这个 Cookie 的生命周期就结束了;而 Session 的生命周期是间隔式的, 当我们通过 session.setMaxInactiveInterval 方法设置 Session 的生命周期为 5 min 时,如果在 5 min 内,没有对该 Session 的访问,那么这个 Session 就会被销毁,但是如果在 5 min 内访问过 Session,那么每次访问都会重置 Session 的生命周期。

面试题二:如何解决分布式 Session?

先来思考一下,分布式应用中,Session 会带来什么问题?

举个例子🌰:

小张的浏览器发送的请求,被 nginx 分发到了服务器 A。这时候,在服务器 A 就会生成一个 Session,并在响应头中返回给小张的浏览器一个 Cookie,Cookie 中存储着 SessionId。那么试想,当小张的浏览器过一段时间又向服务端发送一个请求,服务器 A 处于忙碌状态,所以 nginx 将小张的请求分发给了服务器 B,而此时,服务器 B 并没有小张的 Session 信息,问题就暴露出来了。

解决分布式 Session 通常有如下三种方案:

1. 粘性 Session(Sticky Session)

nginx 默认的负载均衡处理策略为轮询策略,我们可以将其改为哈希策略,这种策略会根据用户的 IP 来计算 hash 值,从而让小张发送的请求就一直由于服务器 A 来处理;不过粘性 Session方案也是有一定问题的,如果服务器 A 下线了,那么原本落到这台服务器上的请求就会分配到别的机器上,这样一来,原本用户持有的会话就失效了,用户的体验便会下降。

2. 同步 Session(Session Replication)

小张浏览器的请求被服务器 A 处理之后,会在服务器 A 内部创建 Session。而同步 Session 的思路就是,将服务器 A 内部存储的 Session 信息同步到其他的服务器上,使多台服务器的 Session 保持一致,由于 Session 是同步的,所以能够保证用户的 Session 信息不丢失。不过该方案也是存在问题的,因为 Session 的复制是有成本的,而且如果访问量巨大的情况下,就会导致每台机器都持有很多 Session,浪费了大量资源。

3. 集中 Session(Centralized Session)

集中 Session 的思路是:将原本存储在单机服务器内存中的 Session 集中地存储在譬如像 Redis 这样的设施中。这样就可以保证,用户无论向哪一台服务器发送请求,只要持有相同的 SessionId,那么我们就可以将这些 Session 从集中的会话里取出。

Spring 框架对分布式 Session 解决方案提供了相应的支持:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-core</artifactId>
</dependency>

Spring 便是使用了集中 Session 这种方案来解决分布式 Session 问题,它支持 Redis,MongoDB,JDBC 等多种 Session 存储方式,我们来看一个示例程序:

在该程序中,我引入了 spring-session 依赖,并使用了 @EnableRedisHttpSession 注解,指明使用 Redis 作为 Session 的存储。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

@SpringBootApplication
@RestController
@EnableRedisHttpSession
public class SessionDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SessionDemoApplication.classargs);
    }

    @RequestMapping("/hello")
    public String printSession(HttpSession session, String name) {
        String storedName = (String) session.getAttribute("name");
        if (storedName == null) {
            session.setAttribute("name", name);
            storedName = name;
        }
        return "hello " + storedName;
    }
}

在开启 Redis 服务后,我们运行程序,并在浏览器地址栏输入:

http://localhost:8080/hello?name=springsession

页面返回结果:

hello springsession

查看此时的 Redis 缓存信息:

127.0.0.1:6379> keys *
1) "spring:session:expirations:1618386000000"
2) "spring:session:sessions:expires:d61d1f30-9eec-4bfe-871c-483aeac0894e"
3) "spring:session:sessions:d61d1f30-9eec-4bfe-871c-483aeac0894e"

说明我们的 Session 已经存储到了 Redis 中。

其实,无论是哪一种分布式 Session 的解决方案,都会有一定的缺点。所以在分布式应用的主流策略是,能用 Cookie 存储就用 Cookie 存储,而对于用户的敏感信息,就存放到数据库中。数据库可以做一些集群备份,不至于丢失。为了减轻数据库的查询压力,我们通常还会在数据库之前加一个缓存层,例如 Redis。这种做法就是当前比较主流的做法,技术实现上也相对成熟。

3、Spring MVC 拦截器如何实现?三个方法的执行时机?


如果想要实现一个 Spring MVC 拦截器,我们就要实现核心接口 HandlerInterceptor。HandlerInterceptor 有三个方法:

  1. boolean preHandle
  2. void postHandle
  3. void afterCompletion

preHandle 调用的时机为 Controller 方法处理之前。在链式 Interceptor 中,其执行顺序为按照 Interceptor 声明顺序一个接一个执行。如果返回 false,则中断执行,且并不会进入到 postHandle 与 afterCompletion 方法当中。

postHandle 方法的调用前提为 preHandle 方法返回 true 时,其调用时机为 Controller 方法处理完成后,DispatcherServlet 进行视图渲染之前。也就是说,我们在 postHandle 方法中已经可以对 ModelAndView 对象进行操作了。在链式 Interceptor 中,与 preHandle 方法不同,postHandle 会按照 Interceptor 声明的顺序倒序执行。

afterCompletion 方法调用的前提为 preHandle 方法返回 true 时,调用的时机为 DispatcherServlet 进行视图渲染之后。afterCompletion 方法多用于清理资源。

在实现了 HandlerInterceptor 接口后,我们就要在配置文件中配置我们的拦截器,其格式如下所示:

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/user/index"/>
        <bean class="com.springmvc.intercepter.MyInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

在 Spring Boot 中,我们可以创建一个带有 @Configuration 注解,实现了 WebMvcConfigurer 的配置类,并重写方法 addInterceptors 来简化拦截器配置。

下面我给出一个代码示例:

MyInterceptor

import org.springframework.util.StopWatch;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyInterceptor implements HandlerInterceptor {

    private ThreadLocal<StopWatch> stopWatch = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("----preHandle----");
        StopWatch sw = new StopWatch();
        stopWatch.set(sw);
        sw.start();
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("----postHandle----");
        stopWatch.get().stop();
        stopWatch.get().start();
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("----afterCompletion----");
        StopWatch sw = stopWatch.get();
        sw.stop();
        String method = handler.getClass().getSimpleName();
        if (handler instanceof HandlerMethod) {
            String beanType = ((HandlerMethod) handler).getBeanType().getName();
            String methodName = ((HandlerMethod) handler).getMethod().getName();
            method = beanType + "." + methodName;
        }

        System.out.println("Request URL : " + request.getRequestURI());
        System.out.println("Method : " + method);
        System.out.println("Response Status : " + response.getStatus());
        System.out.println("Total Time Millis : " + sw.getTotalTimeMillis());
        System.out.println("PreHandle ~ PostHandle Time Millis : " + (sw.getTotalTimeMillis() - sw.getLastTaskTimeMillis()));
        System.out.println("Render Time Millis : " + sw.getLastTaskTimeMillis());

        stopWatch.remove();
    }
}

TestController

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

InterceptorDemoApplication

import com.github.interceptordemo.controller.interceptor.MyInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
public class InterceptorDemoApplication implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(InterceptorDemoApplication.classargs);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor())
                .addPathPatterns("/test/**");
    }
}

该程序会拦截所有请求路径为 test 的方法。当我们访问路径 http://localhost:8080/test/hello 时,控制台输出如下:

----preHandle----
----postHandle----
----afterCompletion----
Request URL : /test/hello
Method : com.github.interceptordemo.controller.TestController.hello
Response Status : 200
Total Time Millis : 79
PreHandle ~ PostHandle Time Millis : 76
Render Time Millis : 3

4、Spring MVC 中常用注解有哪些?


Spring MVC 中的注解有很多,我就挑几个重点的,目前还常比较常用的来说了。

  • @RequestMapping
  • @ResponseBody
  • @RequestBody
  • @RestController
  • @PathVariable
  • @RequestParam

@RequestMapping 注解用于处理请求地址映射,可以用在类上,也可以用在方法上。用在类上时,表示类中所有请求方法都会以该地址作为父路径。用在方法上时,我们需要在 @RequestMapping 中指定请求方法,未指定时默认为 Get 请求。在 Spring 4.3 开始,新增了 @GetMapping@PostMapping@DeleteMapping@PatchMapping@PutMapping 这些以请求动词开头的注解,不仅再一次简化了开发,而且也使得这些注解标注的方法意图更加清晰。

@ResponseBody 注解表示该方法返回的结果将直接写到 HTTP 响应正文(ResponseBody)中。通常,在我们异步获取 JSON 数据时使用。

@RequestBody 注解则是将 HTTP 请求正文(RequestBody)插入到方法中,我们通过 @RequestBody 注解可以获取到请求体中的相关信息。

@ResponseStatus 注解的作用是指定返回的 HTTP 响应状态码。

@RestController 注解相当于 @Controller + @ResponseBody 注解。@Controller 注解就不用我们多说了,它是 Spring MVC 的 Controller 处理器,用来创建处理 HTTP 请求。在 Spring 4 之前,我们如果在一个 Controller 中想要获取到 JSON 格式的数据需要 @Controller 注解与 @ResponseBody 注解配合使用,现在我们只需要一个 @RestController 注解就可以搞定了。

@PathVariable 注解用于将请求参数绑定到 URL 路径上,并传入到方法中,譬如:

@GetMapping("/user/{id}")
public String getUser(@PathVariable Integer id){
    return "userId : " + id;
}

当我们访问路径:http://localhost:8080/user/1 时,页面返回结果为:

userId : 1

@RequestParam 也可以将请求参数绑定到 URL 路径上并传入到方法中,不过它与 @PathVariable 的形式不同,它的 URL 的格式是这样的:

http://localhost:8080/user?id=1

@RequestParam 注解中,有两个属性比较常用,第一个是 required,当 required 属性值为 true 时,表示该参数必须要传递,否则将返回 404,false 则表示为路径后面的参数可有可无;第二个属性为 defaultValue,defaultValue 属性的值会成为请求中没有同名参数时的默认值。

总结

因为 Spring MVC 这一部分本来就不是原计划的内容,所以我就简单写了几个面试题。其实漏掉的的内容还有很多,譬如:请求转发与重定向的区别? Spring MVC 使用到的设计模式?等等... ...

这些内容就交给童鞋们自行补充完成了(Spring MVC 都是这么老的东西了,真的随便搜一搜就有了)。

好啦,至此为止,这篇文章就到这里了,感谢您的阅读与支持~~欢迎大家关注我的公众号,在这里希望你可以收获更多的知识,我们下一期再见!

jrh

2021/10/30  阅读:28  主题:橙心

作者介绍

jrh