抖机灵的面试题

以0为下标

问题:为什么Java,C等大多数编程语言下标都是以0开头的。

因为是基于计算机内存寻址特性来的,因为是a=b+(i*c),所以用零开头,可以少一次计算,如果从1开始就是a=b+(i-1)*c,会多一次计算。

网关

在微服务中,网关都有什么职责,鉴权的话,网关鉴过权了拿了token请求后面服务,那后面服务还需要去验证token有效性么,去网关验证么?能说出非对称加密就算过关。

遵循RESTful 规范,还是只用POST

问题:接口只用post还是post、get、update、delete都用,为什么,只用一种不行么。

只用 POST 可以吗?(RPC 风格)

可以。 这种所有接口都用 POST(或者 GET)的做法,本质上属于 RPC(远程过程调用)风格。 在这种风格下,URL 只代表一个“动作的执行入口”,比如:

  • POST /api/getParkingRecord
  • POST /api/createParkingRecord
  • POST /api/updateParkingRecord

优点:前端和后端都不需要思考该用什么动词,无脑 POST 传 JSON 即可;传参方式统一,不需要管是放在 URL 路径、Query 参数还是 Body 里。许多早期的系统、内部微服务调用,甚至像 GraphQL 这样的现代查询语言,底层都是全靠 POST 支撑的。


为什么要区分 GET、POST、PUT、DELETE?(RESTful 风格)

现代 Web 开发更推崇 RESTful 风格。它把网络上的所有事物都看作“资源”,而 HTTP 方法就是对这些资源操作的“动词”。区分使用它们有以下几个决定性的工程原因:

语义化与代码的自解释性(契约精神)

使用不同的 HTTP 方法,能让接口像自然语言一样清晰易懂。 在开发停车管理系统时,针对“停车记录(parking-records)”这个资源,标准的做法是:

  • GET /api/parking-records/{id} -> 语义:查询某条停车记录。
  • POST /api/parking-records -> 语义:车辆入库,新建一条停车记录。
  • PUT /api/parking-records/{id} -> 语义:更新这条记录(比如结算费用、状态改为已出库)。
  • DELETE /api/parking-records/{id} -> 语义:删除这条错误录入的记录。

前端只要看到 URL 和 HTTP 方法,不看长篇大论的文档就能猜到接口是干嘛的。而如果全用 POST,URL 就会千奇百怪,起名全靠开发者的英语词汇量。

幂等性与分布式重试机制(极度关键)

幂等性(Idempotency)是指:一个操作执行一次和执行一百次,对系统状态产生的影响是相同的。 在复杂的网络环境或云原生架构中,网络超时是常态,调用失败了系统往往会自动重试。

  • GET、PUT、DELETE 是天生幂等的
  • GET 一百次,数据还是那样。
  • PUT(覆盖更新)一百次,数据状态是一致的。
  • DELETE 一百次,结果都是数据不存在。
  • 结论:如果网络超时,网关层或调用方可以安全地自动重试这些请求。
  • POST 不是幂等的
  • POST 是新建。如果前端点击“入库”时网络卡顿触发了重试,而后端全用的是 POST,可能会导致数据库里插入了两条完全一样的入库记录。
  • 结论:区分方法后,系统就能明确知道哪些请求可以安全重试,哪些绝对不能自动重试。

缓存友好性(大幅提升性能)

浏览器、CDN 节点以及像 Nginx 这样的反向代理服务器,天生对 HTTP 方法有不同的缓存策略。

  • GET 请求可以被极其方便地缓存。如果你有一个查询静态配置或字典数据的接口,使用 GET,浏览器或网关会自动帮你缓存结果,极大地减轻后端的数据库压力。
  • POST 请求默认是不缓存的。如果全用 POST,相当于你主动放弃了 HTTP 协议自带的强大缓存机制,所有请求都会生硬地砸到你的 Java 服务上。

契合云原生基础设施与监控

在微服务、网关(如 K8s Ingress)以及现代 AIOps 运维监控体系中,底层基础设施是高度依赖 HTTP 标准的。

  • 运维人员或 AIOps 工具可以通过分析网关日志中的 HTTP 方法,快速得出当前系统的读写比例(GET vs POST/PUT)。
  • 可以针对方法做限流:比如大促期间,为了保命,网关层可以直接降级或者限流所有的 PUT/POST 写操作,但放行 GET 读操作。如果所有接口都是 POST,网关层就抓瞎了,根本分不清哪个是读、哪个是写,只能一刀切。

HTTP有几种请求类型

GET(查)

幂等

POST(增)

非幂等

PUT(改 – 全量替换)

PUT 的语义是替换(Replace)目标资源。

  • 规则:当你使用 PUT 时,客户端需要把该资源的所有字段(完整对象)发送给服务器。如果某个原本存在的字段你在 PUT 请求中没有传,按照严格的 REST 规范,服务器应该将该字段置为 null 或默认值(即抹除旧数据)。
  • 特性:它是幂等的。你把完整的数据覆盖一次和覆盖一百次,服务器上的最终状态都是一样的。
  • 场景:前端表单提供了该数据的所有字段,用户修改完后整体提交保存。

DELETE(删)

幂等

PATCH(改 – 局部打补丁,现代 RESTful 极力推荐的第 5 种)

PATCH 的语义是给资源打补丁(Modify/Patch)

  • 规则:客户端只需要发送需要被修改的字段,不需要传完整的对象。服务器接收到请求后,只会更新你传过来的那几个字段,其余字段保持原样。
  • 特性:按照 HTTP 规范,它不一定是幂等的(尽管在实际的 Java 业务开发中,大部分开发者都会把它写成幂等的直接赋值操作)。
  • 场景:比如在停车场系统中,仅更新一辆车的“出库状态”或“应缴费用”,而不动它的“车牌号”和“入库时间”。

OPTIONS(查 – 并非用来操作业务资源,而是供浏览器和跨域网关进行能力探测,属于基础设施层面的必要补充)

OPTIONS 是一种极其特殊且极其重要的 HTTP 请求方法。它的核心作用是:“投石问路”(查询服务器的通信选项和能力)。

对于前端和后端开发者来说,你遇到 OPTIONS 请求 99% 都是因为一个场景:CORS(跨域资源共享)的预检请求(Preflight Request)

为什么会有 OPTIONS 预检请求?

当你的前端应用(比如运行在 http://localhost:5173)试图向不同域名的后端接口(比如 http://api.yourdomain.com)发送一个复杂请求时,浏览器出于安全保护机制,不会直接把你的实际请求发出去

所谓“复杂请求”,通常包括:

  • 使用了 PUTPATCHDELETE 方法。
  • POST 请求的 Content-Typeapplication/json
  • 请求头里带了自定义的头部(比如 Authorization: Bearer token)。

这时候,浏览器的行为是:

  1. 暗中拦截:浏览器悄悄拦截你的真实请求。
  2. 发送 OPTIONS 投石问路:浏览器自动向相同的后端 URL 发送一个 OPTIONS 请求。这个请求不带 body 数据,而是带上几个特殊的 Header(比如问服务器:Access-Control-Request-Method: PUT,我能用 PUT 吗?)。
  3. 服务器响应:如果你的后端配置了允许跨域(CORS),就会在响应头里告诉浏览器(比如 Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS,可以,你发吧)。
  4. 发送真实请求:浏览器收到允许的许可后,才会把那个真正的 PUTPATCH 请求发出去。

这就是为什么你在浏览器的 Network(网络)面板里,明明代码里只写了一个请求,却经常会看到两个挨着的网络请求记录:一个是 OPTIONS,紧接着才是你的实际请求。

总结OPTIONS 不是用来传业务数据的,它是浏览器和服务器之间为了跨域安全进行的一次握手谈判。

发送 OPTIONS 的好处是什么?(既然慢,为什么还要发?)

前面我们提到 OPTIONS 预检请求会让第一次跨域变慢,很多人觉得这完全是“脱裤子放屁”。但实际上,它是为了保护后端服务器不被恶意攻击

请想象这样一个危险的场景:

  1. 你登录了某个银行网站 bank.com,浏览器里存了银行的登录 Cookie。
  2. 你不小心点开了一个黑客的恶意网站 evil.com
  3. 黑客的网站在后台偷偷用 JavaScript 向 bank.com/api/transfer 发送了一个 DELETE 请求(或者一个带特殊指令的 PUT 请求),试图破坏你的账户。

如果没有 OPTIONS 预检机制: 浏览器会傻乎乎地把这个 DELETE 请求发给银行服务器。虽然银行服务器处理完后,浏览器根据同源策略会拦截响应,不让黑客网站看到结果,但服务器上的执行已经完成了,数据已经被破坏了!

有了 OPTIONS 预检机制(好处体现):

  1. 浏览器发现 evil.com 试图向 bank.com 发送高风险的 DELETE 请求。
  2. 浏览器主动拦截,先发一个没有危害的 OPTIONS 去问银行服务器:“evil.com 想发 DELETE,你允许吗?”
  3. 银行服务器一看,我只允许 bank.com 的请求,于是拒绝了 OPTIONS
  4. 浏览器收到拒绝后,直接把那个真实的 DELETE 请求掐死在摇篮里,根本不往外发。

总结 OPTIONS 的好处:它赋予了浏览器在“危险动作发生之前”就踩刹车的能力,保护了那些没有防范意识的老旧服务器免受跨域的跨站请求伪造(CSRF)类攻击,确保不会产生意外的数据修改(副作用)

先发送options,再发送真实请求,请求变慢的问题。

直观上来说,它确实会让请求变慢。

因为在发送真实的业务请求之前,浏览器必须先和服务器进行一次完整的 HTTP 通信(也就是 OPTIONS 请求)。这在网络术语中叫做增加了一个 RTT(Round Trip Time,往返时延)

假设你的前端和后端服务器之间的网络单程延迟是 50 毫秒:

  1. OPTIONS 请求发过去(50ms)+ 服务器同意并返回(50ms)= 耗时 100ms。
  2. 真实的 POSTPUT 请求再发过去(50ms)+ 服务器处理并返回数据(比如 50ms)= 耗时 100ms。

原本只需要 100ms 就能完成的操作,因为跨域预检,总耗时变成了 200ms。如果网络环境较差,或者跨国访问,这个延迟就会非常明显,用户会感觉到明显的卡顿。

那么,业界是如何解决这个“变慢”的问题的?

为了防止每一个接口调用都遭受这种性能惩罚,浏览器和现代架构主要有以下几种应对策略:

浏览器的终极武器:预检缓存(Max-Age)

浏览器非常聪明,它知道每次都问服务器“我能不能发 PUT”很蠢。所以,HTTP 协议提供了一个专门的响应头:Access-Control-Max-Age

  • 原理:当后端服务器在响应 OPTIONS 请求时,可以带上这个 Header,比如 Access-Control-Max-Age: 86400(单位是秒,即 24 小时)。
  • 效果:浏览器收到这个通行证后,会把它缓存在本地。在接下来的 24 小时内,只要前端再向同一个接口发送相同类型的请求,浏览器就直接放行真实的请求,不再发送 OPTIONS 去探路了。
  • 结论:实际上,只有用户打开网页的第一次复杂请求会变慢,后续的请求速度和没有跨域是一模一样的。
避免触发预检:使用“简单请求”

如果你的请求完全符合“简单请求”的严苛条件,浏览器就不会发 OPTIONS,而是直接发真实请求。 但这在现代开发中极难做到,因为简单请求要求:

  • 只能是 GET、POST、HEAD。
  • HTTP Header 只能用最基础的那几个(不能带 Authorization 传 Token)。
  • POST 的 Content-Type 只能是表单格式(application/x-www-form-urlencoded 等),不能是 application/json

既然现在企业级开发(比如你的停车管理系统)几乎全是用 JSON 交互,并且必然会带 Token 进行权限校验,所以触发预检几乎是不可避免的

终极架构方案:彻底消灭跨域(同源部署)

既然跨域(CORS)会带来这些麻烦,最一劳永逸的方法就是让前端和后端在同一个域下,彻底不跨域

在现代云原生架构中,我们极少让前端直接去请求后端的真实域名,而是通过网关或反向代理来统一入口。比如在 Kubernetes 环境下,你可以利用 Ingress 或 K3s 自带的 Traefik 来做路由转发:

  • 假设你的主域名是 www.parking-system.com
  • 前端页面部署在集群中,Ingress 配置路由:当用户访问 www.parking-system.com/ 时,流量打到前端服务的 Pod。
  • 后端 Java 服务也部署在集群中,Ingress 配置路由:当请求路径匹配 www.parking-system.com/api/* 时,流量自动转发给后端的 Spring Boot 服务。

这样做的结果是:对于浏览器而言,前端代码和后端接口都在同一个域名下。浏览器认为这是同源请求,安全得很,于是彻底关闭了 CORS 机制,再也不会发送任何 OPTIONS 预检请求,性能达到最优。

总结来说:OPTIONS 确实会造成首次请求的延迟,但在良好的服务端缓存配置(Max-Age)或优秀的网关架构(同源反向代理)下,这种性能损耗可以被完美抹平。

HEAD

HEAD 请求本质上就是“没有响应体的 GET 请求”。

当你向服务器发送 GET 请求时,服务器会返回响应头(Headers)和响应体(Body,比如真实的 JSON 数据或图片文件)。 而当你发送 HEAD 请求时,服务器只返回响应头(Headers),绝对不会返回 Body。

它的核心使用场景是:在不消耗大量带宽的情况下,获取资源的元信息(Meta-information)。

  • 场景一:大文件下载前的探测。 假设你要下载一个几个 G 的视频文件。你可以先发一个 HEAD 请求,查看响应头里的 Content-Length,提前知道文件有多大,从而决定是否要开始真正的 GET 下载,或者用来在前端显示进度条的总大小。
  • 场景二:检查链接是否失效(死链检测)。 爬虫或监控系统需要定期检查几万个网址是否还能访问。如果用 GET,会把网页全部拉下来,极其浪费流量和内存。用 HEAD 请求,只要看到返回的 HTTP 状态码是 200 OK,就知道链接活着;如果是 404,就知道链接失效了。
  • 场景三:缓存验证。 查看响应头中的 Last-Modified(最后修改时间)或 ETag,判断服务器上的文件自从上次访问后有没有被修改过。如果没有修改,就不需要重新获取。

TRACE

作用:它是一个“回显(Echo)”服务。你发给服务器什么请求,服务器就把你发的东西原封不动地在响应体里返回给你。

场景:主要用于网络诊断。你的请求在到达最终服务器之前,可能会经过很多代理服务器(Proxy、网关)。使用 TRACE,你可以看到最终服务器收到的请求是什么样子的,从而排查到底是哪个中间节点篡改了你的 Header。

注意:在生产环境中,通常会禁用 TRACE 请求,因为它可能会引发一种叫 XST(跨站追踪)的安全漏洞。

CONNECT

作用:要求代理服务器将连接转换为透明的 TCP/IP 隧道。

场景:它几乎专门用于代理服务器(Proxy)环境下的 HTTPS 穿透。当你在公司内网通过代理上网时,你的浏览器会向代理服务器发送 CONNECT 请求,代理服务器帮你和外部目标网站建立一条盲目的数据通道,然后你的浏览器才开始在这条通道里进行加密的 HTTPS 握手。

架构师

Q:你觉得怎么才算一个合格的架构师,或者成长之路是什么

A:架构师也许要很多技能,但自己觉得架构师一个重要的点就是呆过适当多的公司,并且在公司里呆过一段时间,熟悉公司的流程,然后公司这段时间也有不断新来的人,这样才能在尽可能短的时间内与别人充分的交流,去陈纳新。当然他自己也要有爱学精神,不断钻研新的技术与趋势。

service、serviceImpl

问题:java项目为什么要拆分为service,serviceImpl ,这都是一个人写的,不就重复了么,写一个就行了。

Spring 框架的核心机制:AOP 代理

这是在 Spring/SpringBoot 项目中最实际的一个原因。Spring 的核心特性之一是 AOP(面向切面编程),你平时用的 @Transactional(事务控制)、@Cacheable(缓存)、以及自定义的权限校验注解,底层都是通过 AOP 实现的。

  • JDK 动态代理:Spring AOP 默认优先使用 JDK 动态代理,而 JDK 动态代理强制要求目标类必须实现一个接口
  • 如果你不写接口直接写类,Spring 只能退而求其次使用 CGLIB 通过生成子类的方式来创建代理对象。虽然现代版本的 Spring 已经很好地支持了 CGLIB,但基于接口的代理依然是最标准、最不容易出代理失效问题的做法。

面向接口编程与依赖倒置原则(SOLID)

在软件工程中,有一条非常重要的原则:依赖于抽象(接口),而不是依赖于具体实现

  • 解耦:当你的 Controller 调用 Service 时,Controller 只关心 Service 能提供什么功能(接口定义的方法),而不关心它是怎么实现的。
  • 扩展性:假设你的项目中有一个 FileStorageService,一开始你只写了一个本地存储的实现 LocalFileStorageServiceImpl。后期业务发展,需要接入阿里云 OSS,你只需要再写一个 OssFileStorageServiceImpl 实现该接口,并在配置中切换注入即可,Controller 层的代码一行都不用改。

定义规范与契约(团队协作)

虽然现在是你一个人写,但在企业级开发中,往往是前后端分离或多人协作:

  • 提前交付契约:项目初期,你可以先把 Controller 和 Service 的接口统一定义好,不仅前端可以根据接口文档先去 Mock 数据开发,团队里的其他后端成员也可以直接调用你的 Service 接口进行联调,而不需要等你写完里面几百行的具体业务逻辑。
  • 接口就像是一份“合同”,清清楚楚地列出了模块提供的能力,看接口比看混杂着各种 if-else 的实现类要清晰得多。

分布式系统与微服务调用(RPC)

如果你以后接触到微服务架构(比如 Spring Cloud Feign 或 Dubbo),接口的作用就极其关键了。

  • 在服务间的 RPC 调用中,服务提供方会将 Service 接口打包成一个 API Jar 包提供给消费方。
  • 调用方只需要引入这个含有接口的依赖,就能像调用本地方法一样调用远程服务,而无需知道远程到底是怎么实现的。此时,接口就是微服务之间通信的标准协议。

RPC抄过来的,接口打包成jar依赖给别人。自己负责维护Impl实现,大厂都是这么干的。不过单体或者SpringCloud就没必要了,你分析的都很对。可能是没有大厂的命,都得了大厂的病。

其实接口层没有也没事,但接口层也有他的好处,就是多人维护一个项目的时候,接口层相当于一个简单的功能说明,也就是所对应的实现类对外开放了多少口子。
像一些大的项目,实现类代码都好几千行了,里面也包含了大量不对外的私有方法,当你要找一个功能或者了解一个实现类有什么功能的时候就会很乱。但接口层只有对外接口的声明,就显得很清晰

为什么内存比磁盘块,为什么SSD比机械硬盘快

内存比磁盘更靠近CPU,所以读写数据会更快,比内存更快的是CPU自带的三级缓存。SSD是直接电的速度在运行,而机械硬盘读取数据要靠磁盘转,所以SSD比机械硬盘快。

为什么k8s里最小调度单位是pod,而不是容器

pod里面不会仅有一个容器的,有时候是容器组,比方说用了istio会多两个容器,用了knative会多一个容器,所以pod是容器组的概念,他们共用网络,存储等,所以最小调度单位不是容器而是pod。

自述

自我描述和突出点。面试别人自己问,还不如让对方主动说,让他自己主动说做了什么,以及项目中的闪光点,这不仅考验面试者的表达能力,也考验面试者的总结能力。如果自己想不到,那么主动问也没能问出啥,自己可以适当的提问。

版权声明:除特殊说明,博客文章均为Mark原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。VIP内容严禁转载! | 广告招租请留言
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇