技术演进中的开发沉思-158 java-servlet: applet 程序 (上)

记得那年我在科研所的机房里敲完第 17 个 Servlet 客户端方法,手指在键盘上僵了半分钟 —— 屏幕上的代码像复制粘贴出来的双胞胎:都是创建缓冲区、写请求头、发数据、读响应,唯一的区别只是方法名和参数类型。那时我就想:要是能让计算机替我写这些重复代码,我就能早点回家陪刚满月的女儿了。

如今翻到这一章,像是看到了当年的心愿照进现实。这一章的核心,就是教我们打造一个 “代码小助手”,把 Servlet 客户端和服务器端那些 “换汤不换药” 的工作自动化。

一、 编写客户程序总是大同小异

做程序员久了会发现,很多工作就像餐馆里的流水线:洗菜、切菜、下锅,步骤永远不变,变的只是食材。Servlet 客户端程序就是如此 —— 无论你是做加法计算,还是查赛车记录,核心流程永远逃不出三步。

上一章我们写过客户端代理,当时没细想,现在回头看,所有方法的骨架都一模一样(图 11.1、图 11.2)。就拿 “lite” 版本的add方法来说(图 11.1):第一步创建ByteArrayOutputStream当内存缓冲区,像准备一个干净的菜板;第二步调用_createHeader做请求头,好比给食材套上保鲜膜;第三步用_invokeMethod发请求、读响应,就是把食材下锅再装盘。哪怕换成正规 HTTP 隧道的query方法(图 11.2),也只是把 “菜板” 换成了ResultSet,“保鲜膜” 换成了PreparedStatement,流程半分没变。

public double add(double a, double b) {

double n = 0;

try {

// 1. 准备缓冲区(菜板)

ByteArrayOutputStream baos = new ByteArrayOutputStream();

// 2. 构建请求头(保鲜膜)

DataOutputStream out = (DataOutputStream) _createHeader(baos, 0);

// 填参数(放食材)

out.writeDouble(a);

out.writeDouble(b);

// 3. 发请求+读响应(下锅+装盘)

DataInputStream in = (DataInputStream) _invokeMethod(baos.toByteArray());

// 取结果(盛菜)

n = in.readDouble();

// 收尾(洗工具)

out.close();

in.close();

} catch (Exception ex) {

ex.printStackTrace();

}

return n;

}

更有意思的是,“lite” 版本和正规版本的区别,本质上只是 “工具材质” 不同。“lite” 用DataInputStream/DataOutputStream,兼容老版 JDK(比如 1.0.2),像用不锈钢锅,耐用但功能简单;正规版本用ObjectInputStream/ObjectOutputStream,能传自定义对象(比如IndyRecord),好比用不粘锅,能处理更复杂的食材,但对 “炉灶”(JDK 版本)有要求。

可不管用什么工具,程序员都在重复劳动。我当年写过一个物流查询的客户端,12 个方法,每个方法都要复制粘贴这套流程,改改参数名和返回值 —— 现在想起来,那些深夜里敲键盘的声音,更像是机械的重复,而非创造。

如果说客户端程序是餐馆的前台流水线,那服务器端程序就是后厨的备餐流程 —— 步骤更繁琐,但同样重复。你回忆一下,上一章写的服务器代码存根,是不是都在走这七步:

  • 读客户端请求(接订单);
  • 拿服务器对象实例(找厨师);
  • 建响应头(准备餐盘);
  • 开内存缓冲区(铺餐布);
  • 读方法序号(看订单要什么菜);
  • 调用对应方法(做菜);
  • 发响应(送菜上桌)。

 “lite” 隧道服务端代码,和正规隧道代码,完美印证了这一点。服务端的核心是_invokeMethod方法,里面用switch(ordinal)匹配方法序号 ——ordinal=0调add,ordinal=1调subtract,像厨师看订单编号做菜:#0 是番茄炒蛋,#1 是青椒肉丝,流程都是切菜、下锅、调味。

public void _invokeMethod(Object serverObject, int ordinal, DataInput in, DataOutput out) throws Exception {

// 2. 拿服务器对象(找厨师:Math类负责计算)

Math math = (Math) serverObject;

DataInputStream dataIn = (DataInputStream) in;

DataOutputStream dataOut = (DataOutputStream) out;

// 5. 读方法序号(看订单编号)

switch (ordinal) {

case 0: // add(#0号菜:加法)

// 6. 读参数+调用方法(备菜+做菜)

double a0 = dataIn.readDouble();

double b0 = dataIn.readDouble();

double n0 = math.add(a0, b0);

// 7. 写返回值(装盘送出去)

out.writeDouble(n0);

break;

case 1: // subtract(#1号菜:减法)

double a1 = dataIn.readDouble();

double b1 = dataIn.readDouble();

double n1 = math.subtract(a1, b1);

out.writeDouble(n1);

break;

// ... 其他方法(其他菜品)

default:

throw new Exception("Invalid ordinal: " + ordinal); // 没这个菜

}

}

正规隧道的服务端代码也是同理,只是把 “食材处理工具” 从DataInputStream换成了ObjectInputStream,能处理IndyRecord这种自定义对象 —— 好比厨师换了套能处理海鲜的工具,但备餐流程还是老样子。

我当年维护过一个电商项目的服务器端,20 多个接口,每个接口的服务端代码都要写一遍switch(ordinal)。有次手滑把case 3写成了case 5,线上报了一堆 “无效方法序号” 的错,排查了半天才找到 —— 现在想,这种重复劳动不仅浪费时间,还容易出错。就像厨师天天重复切菜,总有一天会切到手。

二、让 Java 为你编写客户端和服务器

既然客户端和服务器端代码都在重复,那能不能让 Java 替我们写?这个想法在 2002 年第一次冒出来时,我还以为是异想天开 —— 直到发现了 Reflection API,才知道 Java 早就留了 “后门”。

要实现自动化,得解决三个问题:首先,得知道要生成什么代码(确定 “食材”);其次,得能 “看透” 类的结构(知道 “食材” 怎么处理);最后,得能把代码写出来(把 “菜” 做出来)。这三点,正好对应我们要做的三件事:定义接口、用 Reflection API 分析类、用模板生成代码。

2.1 使用 Reflection API:ShowClass

Reflection API 就像给 Java 装了一副 X 光眼镜 —— 不管是类的方法、属性,还是父类、接口,都能看得一清二楚。JDK 1.1 之后就有了这个功能,藏在java.lang.reflect包下,核心是java.lang.Class类 —— 所有 Java 类的 “祖宗”,它的方法(表 11.1)能把类的五脏六腑都扒出来。

关键 Reflection 方法

方法

作用描述

getDeclaredMethods

获取类中自己声明的所有方法(不包括继承的),像看一个人自己会的技能

getInterfaces

获取类实现的所有接口,像看一个人加入的所有社团

getSuperclass

获取类的直接父类,像看一个人的父亲

getModifiers

获取类的修饰符(如 public、private),像看一个人的身份标签

getFields

获取类的所有字段(包括继承的),像看一个人身上带的所有物品

为了用好这副 “X 光眼镜”,我们写了个 ShowClass 程序(图 11.5),功能比 JDK 自带的javap还直观 —— 输入类名,就能把类的继承链、接口、方法全列出来,像给类拍了张全身 CT。

ShowClass 的核心是go方法,流程很简单:先通过Class.forName(className)加载类(好比找到要拍 CT 的人),再用getSuperClasses查父类(查家族史)、getInterfaces查接口(查社团)、getMethods查方法(查技能),最后把信息打印出来(出 CT 报告)。

public void go(String className) {

try {

// 1. 加载类(找到要拍CT的人)

Class c = Class.forName(className);

// 2. 查父类(家族史)

Vector extendList = getSuperClasses(c);

// 3. 查接口(社团)

Vector interfaceList = getInterfaces(c);

// 4. 查方法(技能)

Vector methods = getMethods(c);

// 5. 打印结果(出CT报告)

System.out.println(getModifierString(c.getModifiers()) + " " + c.getName());

printList("extends", extendList); // 打印父类

printList("implements", interfaceList); // 打印接口

printList("Methods", methods); // 打印方法

} catch (ClassNotFoundException ex) {

System.out.println("Class '" + className + "' not found."); // 没找到人

}

}

当年我用 ShowClass 排查过一个经典 bug:有个同事写的 Servlet,客户端调用query方法总报 “参数类型不匹配”,查了半天没找到问题。我用 ShowClass 扫了一眼服务端类,发现接口里定义的是query(int year),但他实际实现时写成了query(Integer year)—— 就像 CT 照出了隐藏的肿瘤,Reflection API 一下就揪出了这个类型错误。

不过要注意,当年很多浏览器把 Reflection API 当成 “危险工具”—— 就像医院不会随便给健康人拍 CT,浏览器也不允许 Applet 用 Reflection,怕它 “偷看” 本地类的结构。所以我们当时有个规矩:Reflection 只在服务端用,生成代码时 “偷偷” 用,绝不放 Applet 里。

2.2 编写 ServletGen

有了 Reflection API 这副 “X 光眼镜”,下一步就是打造 “代码小助手”——ServletGen。它的核心思路很简单:用模板当 “菜谱”,用 Reflection API 当 “备菜工具”,自动生成客户端和服务器端代码。

用模板当 “菜谱”

写代码就像做菜,菜谱上会写 “放 2 勺盐”“炒 3 分钟”,模板里就会写 “生成客户端类名”“插入方法代码”。图 11.11 是客户端代理模板,里面的%CLIENT_NAME%、%METHODS%就是 “菜谱里的空白处”,等着 ServletGen 填空。

模板里还有个小细节:每行开头的#号是用来控制缩进的 —— 一个#对应一层缩进(比如 4 个空格)。有人喜欢紧凑的代码,有人喜欢宽松的,改改#号的替换规则就行,像菜谱里 “盐放多少” 可以按口味调。

// 图11.11 客户端代理模板(关键部分)

/*

* 生成的类名:%CLIENT_NAME%(如MathClient)

* 生成时间:%TIMESTAMP%(如2025-10-28 15:30:00)

*/

%PACKAGE_STATEMENT% // 包声明(如package com.example;)

import java.io.*;

import javaservlets.tunnel.client.*;

public class %CLIENT_NAME%

#extends %SUPER_CLASS% // 父类(如BaseClient)

#implements %INTERFACE_NAME% // 接口(如MathInterface)

{

#// 构造方法(固定模板)

#public %CLIENT_NAME%(String url) throws TunnelException, IOException {

###_setURL(new java.net.URL(url));

###_initialize();

#}

#%METHODS% // 这里自动生成所有接口方法(如add、subtract)

}

让 ServletGen “按菜谱做菜”

ServletGen 的核心是 BaseCodeGen 类(图 11.12),它就像 “厨师长”,负责读模板、处理标识符、生成代码。流程分三步:

  • 读模板:打开模板文件,像厨师拿出菜谱;
  • 填模板:用 Reflection API 获取接口信息,替换模板里的%XXX%(比如把%CLIENT_NAME%换成MathClient),像按菜谱放食材;
  • 写代码:把填好的模板写入.java文件,像把做好的菜装盘。

其中最关键的是processTag方法,它负责处理模板里的标识符。比如遇到%METHODS%,就调用processMethods方法,用 Reflection API 扫接口的所有方法,自动生成每个方法的代码 —— 就像厨师看到菜谱里 “做 10 道菜”,就按顺序把 10 道菜全做出来。

protected String processTag(String line, String tag, int pos, int numIndent, PrintWriter out) throws IOException {

String code = null;

if (tag.equals("METHODS")) {

// 用Reflection API扫接口方法,生成代码

Class c = getInterfaceClass(); // 获取接口类

Method[] methods = c.getMethods(); // 扫所有方法

for (Method method : methods) {

if (Modifier.isPublic(method.getModifiers())) {

codeMethod(method, numIndent, out); // 生成单个方法代码

}

}

line = null; // 方法代码直接写输出流,不用替换原行

}

// 处理其他标识符(如%CLIENT_NAME%、%TIMESTAMP%)

// ...

return line;

}

当年第一次用 ServletGen 时,我输入MathInterface接口名,不到 1 秒就生成了MathClient(客户端)和MathServer(服务端)两个类,共 300 多行代码。旁边的测试工程师看了说:“你这比我写测试用例还快!”—— 那一刻,我想起了当年深夜敲重复代码的日子,突然觉得,技术的进步,就是让程序员少做 “机械活”,多做 “创造性活”。

2.3 隧道实例再访:RemoteMathLite

光说不练假把式,我们用 “RemoteMathLite” 实例,看看 ServletGen 到底好不好用。这个实例是 “lite” 版本的 HTTP 隧道,实现简单的加减乘除计算,核心是MathInterface接口(定义add、subtract等方法)。

第一步:定义接口(定 “食材清单”)

先写MathInterface接口,里面就 4 个方法:add、subtract、multiply、divide—— 像给 ServletGen 列了份 “食材清单”,告诉它要生成哪些方法的代码。

// MathInterface接口(食材清单)

public interface MathInterface {

double add(double a, double b);

double subtract(double a, double b);

double multiply(double a, double b);

double divide(double a, double b);

}

第二步:用 ServletGen 生成代码(按清单做菜)

运行 ServletGen,输入接口名MathInterface,选择 “lite” 版本模板,它会自动做三件事:

  • 生成客户端MathClientLite:里面的每个方法都遵循 “缓冲区→请求头→发请求→读响应” 的流程(图 11.1 的风格),不用我们手写;
  • 生成服务器端MathServerLite:里面的_invokeMethod方法用switch(ordinal)匹配方法,参数和返回值都用DataInputStream/DataOutputStream处理;
  • 生成配置文件:告诉 Servlet 容器,哪个 URL 对应哪个服务端类(如/math对应MathServerLite)。


// 测试RemoteMathLite

public class MathTest {

public static void main(String[] args) {

try {

// 1. 创建客户端实例(连接服务端)

MathClientLite client = new MathClientLite("http://localhost:8080/RemoteMathLite/math");

// 2. 调用自动生成的方法(测试加减乘除)

double addResult = client.add(10.5, 2.3);

double subResult = client.subtract(15.8, 4.2);

double mulResult = client.multiply(6.0, 7.5);

double divResult = client.divide(20.0, 4.0);

// 3. 打印结果

System.out.println("10.5 + 2.3 = " + addResult); // 输出 12.8

System.out.println("15.8 - 4.2 = " + subResult); // 输出 11.6

System.out.println("6.0 * 7.5 = " + mulResult); // 输出 45.0

System.out.println("20.0 / 4.0 = " + divResult); // 输出 5.0

// 4. 关闭客户端

client.close();

} catch (Exception e) {

e.printStackTrace();

}

}

}

运行这个测试类时,我特意留意了时间 —— 从编译代码到看到结果,总共不到 2 分钟。对比当年手动写客户端、服务端代码的 2 小时,效率提升了 60 倍。更重要的是,自动生成的代码没有语法错误,也没有 “把 case3 写成 case5” 的低级 bug—— 这就像工厂流水线生产的零件,比手工打磨的更标准、更可靠。

还有个细节值得一提:RemoteMathLite 的服务端MathServerLite,会自动复用第 10 章写的LiteTunnelServlet基础类。就像盖房子时复用预制的墙体,不用再从地基开始砌砖。这种 “复用 + 自动化” 的组合,让我们在 2003 年的科研项目中,把原本需要 1 周的 Servlet 开发时间,压缩到了 1 天。

2.4 隧道实例再访:RemoteIndy

如果说 RemoteMathLite 是 “家常菜”,那 RemoteIndy 就是 “大餐”—— 它是正规 HTTP 隧道的实例,用来查询印第安纳波利斯 500 赛车(Indy 500)的历史记录,核心是处理自定义对象IndyRecord(包含年份、车手、速度三个字段)。

第一步:定义接口与实体类(定 “大餐菜谱”)

先写IndyInterface接口,包含connect(连接数据库)、close(关闭连接)、query(查询记录)三个方法,其中query的返回值是IndyRecord:

// IndyInterface接口

import java.sql.SQLException;

public interface IndyInterface {

// 连接数据库

boolean connect() throws SQLException;

// 关闭数据库连接

void close() throws SQLException;

// 根据年份查询赛车记录

IndyRecord query(int year) throws SQLException;

}

// 自定义实体类IndyRecord(需要实现Serializable接口,支持对象传输)

import java.io.Serializable;

public class IndyRecord implements Serializable {

public int year; // 比赛年份

public String driver; // 冠军车手

public double speed; // 平均速度(英里/小时)

}

第二步:用 ServletGen 生成代码(按 “大餐菜谱” 备菜)

运行 ServletGen 时,选择 “正规隧道模板”,输入IndyInterface接口名。这次生成的代码和 RemoteMathLite 有两个关键区别:

  • 流类型不同:客户端用ObjectInputStream/ObjectOutputStream,支持传输IndyRecord对象;
  • 异常处理不同:自动添加SQLException捕获,符合数据库操作的场景。

生成的客户端IndyClient中,query方法的核心代码如下:

// 自动生成的IndyClient.query方法

public IndyRecord query(int year) throws SQLException {

IndyRecord record = null;

try {

// 1. 准备缓冲区(比lite版本多了对象流初始化)

ByteArrayOutputStream baos = new ByteArrayOutputStream();

ObjectOutputStream out = (ObjectOutputStream) _createHeader(baos, 2); // ordinal=2对应query

// 2. 写入参数(年份)

out.writeInt(year);

// 3. 发送请求并读取响应(用ObjectInputStream读自定义对象)

ObjectInputStream in = (ObjectInputStream) _invokeMethod(baos.toByteArray());

// 4. 读取返回值(直接获取IndyRecord对象)

record = (IndyRecord) in.readObject();

// 5. 关闭流

out.close();

in.close();

} catch (ClassNotFoundException e) {

throw new SQLException("IndyRecord类未找到", e);

} catch (IOException e) {

throw new SQLException("IO异常", e);

}

return record;

}

第三步:测试运行(尝 “大餐”)

部署后,我写了个测试类查询 1965 年的 Indy 500 记录 —— 那年的冠军是 Jim Clark,平均速度 150.686 英里 / 小时:

// 测试RemoteIndy

public class IndyTest {

public static void main(String[] args) {

try {

IndyClient client = new IndyClient("http://localhost:8080/RemoteIndy/indy");

client.connect(); // 连接数据库

// 查询1965年的记录

IndyRecord record = client.query(1965);

if (record != null) {

System.out.println("1965年Indy 500冠军:");

System.out.println("车手:" + record.driver); // 输出 Jim Clark

System.out.println("速度:" + record.speed); // 输出 150.686

}

client.close();

} catch (SQLException e) {

e.printStackTrace();

}

}

}

运行结果和预期完全一致。最让我感慨的是,当年手动写这种带自定义对象传输的 Servlet,光是处理对象序列化、异常转换就花了大半天,而现在用 ServletGen,5 分钟就能搞定全套代码。这让我想起一句话:“好的工具,能让程序员从‘体力劳动者’变回‘脑力劳动者’”。

最后小结

翻完这一章的代码,我又想起 2001 年那个敲重复代码的深夜。那时的我不会想到,十几年后,Spring Boot 的@RestController能自动生成接口,MyBatis 的Mapper能自动生成 SQL—— 但这些技术的核心思想,和我们当年的 ServletGen 其实是相通的:用自动化消除重复劳动,让程序员专注于业务逻辑

总结一下本章的核心收获,其实就三句话:

  • 重复的代码该 “自动化”:客户端的 “缓冲区→请求→响应”、服务端的 “读参数→调方法→写结果”,都是重复劳动,适合用工具自动生成;
  • Reflection API 是 “钥匙”:它能 “看透” 类的结构,让工具知道该生成哪些方法、用什么参数 —— 这是自动化的基础;
  • 模板是 “骨架”:用模板定义代码的固定结构,用标识符填充变量部分,既灵活又易维护,就像用模具批量生产零件。

从桌面开发到 Web 2.0,再到现在的微服务,我经历过很多技术迭代,但 “消除重复” 的原则从未变过。当年我们用 ServletGen 解决的问题,现在可能被更先进的框架替代,但这种 “用技术解决技术痛点” 的思维,才是程序员最该保留的能力。未完待续........

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值