调用后端接口,发现接口返回的数据有两组 json, 非数组。非常奇怪🙉
{
"name": "John",
"age": 30
}
{
"path": "/examples/servlets/servlet/JsonExample",
"code": 500
}
/**
* Servlet 包版本问题,导致 SpringMVC Servlet 打印日志报错
* @see org.springframework.web.servlet.FrameworkServlet#processRequest
*/
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) {
try {
doService(request, response);
}
catch (ServletException | IOException ex) {
}
finally {
// HttpStatus httpStatus = HttpStatus.resolve(status); since 5.0 才有这个方法 ❌
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
第一个 json 是业务逻辑返回内容。controller 逻辑没有发生异常,正常写入 response。
第二个 json 是 SpringMVC Servlet 发生异常后,由 Tomcat ErrorPage 转发到默认的错误处理 Servlet 生成的内容。
两个 json 内容通过 RequestDispatcher.include 方法,都写入到 response。
借助 Tomcat examples 应用,可以快速搭建案例场景。✔
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
// Servlet 中返回 JSON 字符串
public class JsonServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
String json = "{\"name\": \"John\", \"age\": 30}";
PrintWriter out = response.getWriter();
out.print(json);
// 业务内容写入 response, response 变为 submit 状态。
out.flush();
// 模拟上述 SpringMVC 异常
if(true) {
throw new RuntimeException("Bad area ref ");
}
}
}
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
// 对应 SpringBoot 提供的 BasicErrorController
public class ErrorServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String path = request.getRequestURI();
String json = "{\"path\": \"" + path + "\", \"code\": 500}";
PrintWriter out = response.getWriter();
out.print(json);
}
}
<servlet>
<servlet-name>JsonExample</servlet-name>
<servlet-class>JsonServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>JsonExample</servlet-name>
<url-pattern>/servlets/servlet/JsonExample</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>ErrorExample</servlet-name>
<servlet-class>ErrorServlet</servlet-class>
</servlet>
<!-- 对应的注册过程: org.springframework.boot.autoconfigure.web.BasicErrorController -->
<servlet-mapping>
<servlet-name>ErrorExample</servlet-name>
<url-pattern>/error</url-pattern>
</servlet-mapping>
<!-- 对应: ErrorPageRegistry.addErrorPages(errorPage); -->
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/error</location>
</error-page>
cd /d/services/apache-tomcat-8.0.48/webapps/examples/WEB-INF/classes
# /D/repository/javax/servlet/servlet-api/2.4/servlet-api-2.4.jar
javac -cp servlet-api-2.4.jar JsonServlet.java
javac -cp servlet-api-2.4.jar ErrorServlet.java
运行 Tomcat。 访问链接 http://localhost:8080/examples/servlets/servlet/JsonExample
基于《Insight h2database 更新、读写锁以及事务原理》对于更新流程有了深入了解。在独占锁的简单模型上,分析 h2database 基于乐观锁(并发控制机制)的行锁锁定机制。
验证 h2database MVCC 机制,需要有并发的环境,不能再使用 org.h2.tools.Shell
。可以使用 debug 模式,运行 org.h2.tools.Console
。使用不同的浏览器模拟多 session
-- session 1 更新数据并打标
SET AUTOCOMMIT OFF;
update city set code = 'bjx' where id = 9;
-- session 2 读取数据正常
select * from city where id = 9;
-- session 2 更新数据异常,提示 Timeout,其实是并发更新冲突异常
update city set code = 'sjx' where id = 9;
Timeout trying to lock table "CITY";
上述的并发异常,在内部的错误代码为:org.h2.api.ErrorCode#CONCURRENT_UPDATE_1 trying to update the same row from within two connections at the same time
在使用 vue 本地开发前后端功能时,发现配置的代理不能正确的请求到后端服务。
通过观察 nodeJs 日志,发现代理 path rewrite 的路径有问题。
借此阅读 webpack-dev-server 源码,了解其工作原理。
前端 vue 工程的 vue.config.js 代理配置如下:
module.exports = {
devServer: {
proxy: {
// 转发到稳定的线上服务
'/someApi': {
target: "online.foo.com",
changeOrigin: true,
pathRewrite: {
'^/someApi': ''
}
},
// 转发到开发中的服务。
// 由于 devServer 路由规则,导致前端发起的 /someApiExtend/something 请求,并没有转发到 development.foo.com。
'/someApiExtend': {
target: "development.foo.com",
changeOrigin: true,
pathRewrite: {
'^/someApiExtend': ''
}
}
}
}
}
/someApiExtend/something 请求被 ‘/someApi’ 优先处理,Rewrite to 的请求解析为:Extend/something。 直接提示 404。❌
Serves a webpack app. 提供开发环境服务
Updates the browser on changes. 热编译和部署
webpack-dev-server 可用于快速开发应用程序。请查阅 开发指南 开始使用。
其中提到了使用到 http-proxy-middleware 软件包。
通过源码分析可以看到集成使用的方式
/**
* 根据 options 解析集成的组件,注册 express web应用中。
* @returns {void}
* @see 核心实现源码文件 lib\Server.js
*/
setupMiddlewares() {
/**
* @type {Array<Middleware>} 需要的组件集合
*/
let middlewares = [];
// 简单case, 如果配置了压缩,则初始化并加入到组件集合。最终注册到 express。 express.use(compression())
// compress is placed last and uses unshift so that it will be the first middleware used
if (this.options.compress) {
const compression = require("compression");
middlewares.push({ name: "compression", middleware: compression() });
}
// 如上的 compress, 引入并初始化代理配置 createProxyMiddleware
if (this.options.proxy) {
const { createProxyMiddleware } = require("http-proxy-middleware");
const getProxyMiddleware = (proxyConfig) => {
// It is possible to use the `bypass` method without a `target` or `router`.
// However, the proxy middleware has no use in this case, and will fail to instantiate.
if (proxyConfig.target) {
const context = proxyConfig.context || proxyConfig.path;
return createProxyMiddleware(
/** @type {string} */ (context),
proxyConfig
);
}
};
/**
* 遍历 devServer.proxy 配置,并初始化为 proxyMiddleware对象,添加到组件合集里。
*/
/** @type {ProxyConfigArray} */
(this.options.proxy).forEach((proxyConfigOrCallback) => {
// 代理配置集合,
let proxyConfig = typeof proxyConfigOrCallback === "function" ? proxyConfigOrCallback() : proxyConfigOrCallback;
let proxyMiddleware = (getProxyMiddleware(proxyConfig));
middlewares.push({
name: "http-proxy-middleware",
// 通过 handler 初始化配置,简单来说,就是调用 getProxyMiddleware(proxyConfig)
middleware: handler,
});
});
}
// 把组件集合注册到 express web 应用中
middlewares.forEach((middleware) => {
/** @type {import("express").Application} */
(this.app).use(middleware.middleware);
});
}
首先,判断是否启用 proxy, 并引入 http-proxy-middleware
组件
然后,遍历 devServer.proxy 集合配置,生成对应的代理配置对象
最后,把 proxy 配置对象(proxyMiddleware)注册到 express
web 应用中
The one-liner node.js http-proxy middleware for connect, express, next.js
方便理解 webpack-dev-server 集成 http-proxy-middleware 软件包的过程
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use(
'/api',
createProxyMiddleware({
target: 'http://www.example.org/secret',
changeOrigin: true,
}),
);
’/’ matches any path, all requests will be proxied.
‘/api’ matches paths starting with /api
✔ 遇到的疑问,此刻得以解答。
webpack-dev-server 基于 express web 框架实现的一个便于开发环境的 Sever
devServer.proxy 功能是其中一个特性,通过 http-proxy-middleware 实现
proxy 配置path 的规则,参考 Context matching 即可。
webpack-dev-server 的代码结构和实现,对于架构设计具有很大的借鉴意义。👍
阅读 h2 数据库的源码是一项复杂的任务,需要对数据库原理、Java 语言和操作系统有深入的理解。可以从以下几方面入手来完成。
首先,你需要在你的机器上安装和配置好开发环境,包括 JDK、Maven、IDE 调试器等工具。
然后,从 h2 的官方网站 或 GitHub上下载源码。
IDE 导入 h2 数据库源码,根据不同的调试场景,启用不同的模式。
# 约等于 java -cp h2-*.jar org.h2.tools.Console
java -cp h2-*.jar
java -cp h2-*.jar org.h2.tools.Shell
在阅读源码之前,理解 h2 数据库的整体架构和主要组件是非常重要的。可以从官方文档或在线教程中获取这些信息。
官方架构讲解 Architecture
h2 数据库的源码非常多,功能非常丰富,可能无法一次性完全理解。因此,选择一个特定的模块或功能(如查询优化器、存储引擎、事务处理等)作为起点,然后逐步扩大你的阅读范围。
基于的 BTree PageStore 存储引擎更贴近日常工作、便于理解,可以先选取该存储引擎入手。
使用调试器跟踪代码的执行过程,这可以帮助你理解代码的运行逻辑。你可以从一些简单的SQL查询开始,看看它们是如何在 h2 数据库中被处理的。
可以使用上述的本地 Shell 模式开启你的源码之旅。
h2 数据库的源码中有大量的注释,这些注释可以帮助你理解代码的功能和工作原理。
架构类的代码,可以从设计模式中寻找灵感。
算法类的代码,可以从最简化的模型来阅读。
对于无法理解的代码,尝试交给 chat-gpt 解读。
h2 相关的资料比较少,数据库的底层原理是相通的。
借鉴 MySQL 的内部工作原理,相关的书籍来了解 h2 设计理念。
从已有的其他开源数据库中获取设计相关的文档。例如:B+树实现 - MiniOB
尝试修改一些代码,然后编译并运行,看看结果是否符合你的预期。这是理解源码的最好方式之一。
可以从 github issues 来了解运行中的问题和修复思路和方案。
针对同一个功能,从 git 不同版本的源码对比中,学习重构和优化的思路。
在设计理念和原理熟悉后,可以着手针对特定场景进行源码改写练习。
如果你遇到无法理解的代码或问题,可以在 h2 数据库的开发者论坛或邮件列表中寻求帮助。