以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/getParkingRecordPOST /api/createParkingRecordPOST /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)发送一个复杂请求时,浏览器出于安全保护机制,不会直接把你的实际请求发出去。
所谓“复杂请求”,通常包括:
- 使用了
PUT、PATCH、DELETE方法。 POST请求的Content-Type是application/json。- 请求头里带了自定义的头部(比如
Authorization: Bearer token)。
这时候,浏览器的行为是:
- 暗中拦截:浏览器悄悄拦截你的真实请求。
- 发送 OPTIONS 投石问路:浏览器自动向相同的后端 URL 发送一个
OPTIONS请求。这个请求不带 body 数据,而是带上几个特殊的 Header(比如问服务器:Access-Control-Request-Method: PUT,我能用 PUT 吗?)。 - 服务器响应:如果你的后端配置了允许跨域(CORS),就会在响应头里告诉浏览器(比如
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS,可以,你发吧)。 - 发送真实请求:浏览器收到允许的许可后,才会把那个真正的
PUT或PATCH请求发出去。
这就是为什么你在浏览器的 Network(网络)面板里,明明代码里只写了一个请求,却经常会看到两个挨着的网络请求记录:一个是 OPTIONS,紧接着才是你的实际请求。
总结:OPTIONS 不是用来传业务数据的,它是浏览器和服务器之间为了跨域安全进行的一次握手谈判。
发送 OPTIONS 的好处是什么?(既然慢,为什么还要发?)
前面我们提到 OPTIONS 预检请求会让第一次跨域变慢,很多人觉得这完全是“脱裤子放屁”。但实际上,它是为了保护后端服务器不被恶意攻击。
请想象这样一个危险的场景:
- 你登录了某个银行网站
bank.com,浏览器里存了银行的登录 Cookie。 - 你不小心点开了一个黑客的恶意网站
evil.com。 - 黑客的网站在后台偷偷用 JavaScript 向
bank.com/api/transfer发送了一个DELETE请求(或者一个带特殊指令的PUT请求),试图破坏你的账户。
如果没有 OPTIONS 预检机制: 浏览器会傻乎乎地把这个 DELETE 请求发给银行服务器。虽然银行服务器处理完后,浏览器根据同源策略会拦截响应,不让黑客网站看到结果,但服务器上的执行已经完成了,数据已经被破坏了!
有了 OPTIONS 预检机制(好处体现):
- 浏览器发现
evil.com试图向bank.com发送高风险的DELETE请求。 - 浏览器主动拦截,先发一个没有危害的
OPTIONS去问银行服务器:“evil.com想发DELETE,你允许吗?” - 银行服务器一看,我只允许
bank.com的请求,于是拒绝了OPTIONS。 - 浏览器收到拒绝后,直接把那个真实的
DELETE请求掐死在摇篮里,根本不往外发。
总结 OPTIONS 的好处:它赋予了浏览器在“危险动作发生之前”就踩刹车的能力,保护了那些没有防范意识的老旧服务器免受跨域的跨站请求伪造(CSRF)类攻击,确保不会产生意外的数据修改(副作用)。
先发送options,再发送真实请求,请求变慢的问题。
直观上来说,它确实会让请求变慢。
因为在发送真实的业务请求之前,浏览器必须先和服务器进行一次完整的 HTTP 通信(也就是 OPTIONS 请求)。这在网络术语中叫做增加了一个 RTT(Round Trip Time,往返时延)。
假设你的前端和后端服务器之间的网络单程延迟是 50 毫秒:
OPTIONS请求发过去(50ms)+ 服务器同意并返回(50ms)= 耗时 100ms。- 真实的
POST或PUT请求再发过去(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。
自述
自我描述和突出点。面试别人自己问,还不如让对方主动说,让他自己主动说做了什么,以及项目中的闪光点,这不仅考验面试者的表达能力,也考验面试者的总结能力。如果自己想不到,那么主动问也没能问出啥,自己可以适当的提问。
