纯Servlet开发实践:轻量级Java Web方案解析
1. 项目概述为什么选择纯Servlet开发在Spring Boot大行其道的今天重新审视Servlet原生开发似乎有些复古。但最近我在重构一个遗留系统时发现项目里层层嵌套的Spring依赖反而成了维护的负担——一个简单的分页查询接口竟引入了23个间接依赖。这让我决定尝试完全基于Servlet规范搭建一个零依赖的Java Web工程。纯Servlet方案的核心价值在于启动时间从Spring Boot的4.2秒降至0.8秒内存占用减少60%实测从280MB降到110MB部署包大小从48MB缩小到3.7MB彻底避免依赖冲突问题注意这种方案适合中小型Web服务特别是对启动速度和资源消耗敏感的场景。如果是复杂业务系统仍需评估框架带来的开发效率收益。2. 工程结构设计2.1 基础目录规范采用Maven标准目录结构但去除所有非必要配置project-root ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── example │ │ │ ├── servlet │ │ │ ├── filter │ │ │ └── util │ │ ├── resources │ │ └── webapp │ │ ├── WEB-INF │ │ │ └── web.xml │ │ └── static │ └── test └── pom.xml关键点web.xml必须放在WEB-INF下Servlet 3.0虽然支持注解但显式配置更可控static目录存放静态资源避免Servlet处理静态请求按功能划分包结构而非按技术分层2.2 极简pom.xml配置project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdpure-servlet/artifactId version1.0.0/version packagingwar/packaging properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target /properties dependencies !-- 唯一必要依赖 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies build finalName${project.artifactId}/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId version3.3.2/version /plugin /plugins /build /project3. 核心组件实现3.1 Servlet基础封装public abstract class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method req.getMethod().toUpperCase(); try { if (GET.equals(method)) { doGet(req, resp); } else if (POST.equals(method)) { doPost(req, resp); } else { resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } } catch (Exception e) { handleException(e, resp); } } private void handleException(Exception e, HttpServletResponse resp) throws IOException { resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); resp.setContentType(application/json); resp.getWriter().write( {\code\:500,\message\:\ e.getMessage() \} ); } }3.2 路由调度实现不使用任何框架的路由方案WebServlet(/api/*) public class DispatcherServlet extends BaseServlet { private static final MapString, HttpServlet handlers new ConcurrentHashMap(); public static void register(String path, HttpServlet servlet) { handlers.put(path, servlet); } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path req.getPathInfo(); HttpServlet handler handlers.get(path); if (handler ! null) { handler.service(req, resp); } else { resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } }使用示例WebServlet(/user) public class UserServlet extends BaseServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType(application/json); resp.getWriter().write({\name\:\张三\}); } } // 在ServletContextListener中注册 public class AppInitializer implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { DispatcherServlet.register(/user, new UserServlet()); } }4. 关键问题解决方案4.1 依赖管理策略虽然我们追求零依赖但实际开发中可能需要引入少量必要库。建议优先选择单JAR无依赖的库如Gson而非Jackson用optionaltrue/optional标记非核心依赖定期运行mvn dependency:tree检查依赖树4.2 性能优化技巧对象池化对频繁创建的DAO、Service对象使用对象池public class ObjectPoolT { private final SupplierT creator; private final QueueT pool new ConcurrentLinkedQueue(); public ObjectPool(SupplierT creator) { this.creator creator; } public T borrow() { T obj pool.poll(); return obj ! null ? obj : creator.get(); } public void release(T obj) { pool.offer(obj); } }手动控制连接池public class DbUtil { private static final HikariConfig config new HikariConfig(); private static final HikariDataSource ds; static { config.setJdbcUrl(jdbc:mysql://localhost:3306/test); config.setUsername(user); config.setPassword(pass); ds new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }4.3 常见问题排查问题1HTTP 405 Method Not Allowed原因Servlet未实现对应方法如只重写doGet但收到POST请求 解决继承我们提供的BaseServlet统一处理方法分发问题2静态资源被Servlet拦截现象访问/css/style.css返回404 解决在web.xml中添加servlet-mapping servlet-namedefault/servlet-name url-pattern*.css/url-pattern /servlet-mapping问题3中文乱码解决方案添加全局过滤器WebFilter(/*) public class CharsetFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); } }5. 进阶开发模式5.1 轻量级IoC实现200行代码实现基础依赖注入public class Container { private static final MapClass?, Object instances new ConcurrentHashMap(); public static T T get(ClassT type) { return (T) instances.computeIfAbsent(type, k - { try { T instance type.getDeclaredConstructor().newInstance(); for (Field field : type.getDeclaredFields()) { if (field.isAnnotationPresent(Inject.class)) { field.setAccessible(true); field.set(instance, get(field.getType())); } } return instance; } catch (Exception e) { throw new RuntimeException(e); } }); } } Retention(RetentionPolicy.RUNTIME) public interface Inject {}使用示例public class UserService { Inject private UserDao userDao; public User getById(long id) { return userDao.findById(id); } } // 获取实例 UserService service Container.get(UserService.class);5.2 热部署方案开发时避免频繁重启使用Tomcat的reloadable参数Context path/app docBase... reloadabletrue/配合JRebel实现类级别热更新5.3 监控方案不依赖Spring Actuator的实现WebServlet(/admin/metrics) public class MetricsServlet extends BaseServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { Runtime runtime Runtime.getRuntime(); MapString, Object metrics new LinkedHashMap(); metrics.put(heapUsed, runtime.totalMemory() - runtime.freeMemory()); metrics.put(heapMax, runtime.maxMemory()); metrics.put(threadCount, Thread.activeCount()); resp.setContentType(application/json); new Gson().toJson(metrics, resp.getWriter()); } }6. 部署与运维6.1 嵌入式服务器方案不依赖Spring Boot的嵌入式启动public class EmbeddedServer { public static void start() throws Exception { Tomcat tomcat new Tomcat(); tomcat.setPort(8080); Context ctx tomcat.addWebapp(, new File(src/main/webapp).getAbsolutePath()); WebResourceRoot resources new StandardRoot(ctx); resources.addPreResources(new DirResourceSet( resources, /WEB-INF/classes, new File(target/classes).getAbsolutePath(), /)); ctx.setResources(resources); tomcat.start(); tomcat.getServer().await(); } }6.2 生产环境配置建议线程池调优!-- server.xml -- Executor nametomcatThreadPool namePrefixhttp-exec- maxThreads200 minSpareThreads10/关闭开发模式Context reloadablefalse cachingAllowedtrue/7. 与现代技术的结合7.1 兼容Spring方案渐进式迁移策略将Spring作为Servlet挂载WebServlet(urlPatterns /spring/*) public class SpringBridgeServlet extends HttpServlet { private WebApplicationContext ctx; Override public void init() { ctx new AnnotationConfigWebApplicationContext(); ((AnnotationConfigWebApplicationContext)ctx).register(AppConfig.class); ctx.refresh(); } Override protected void service(HttpServletRequest req, HttpServletResponse resp) { DispatcherServlet dispatcher ctx.getBean(DispatcherServlet.class); try { dispatcher.service(req, resp); } catch (Exception e) { throw new ServletException(e); } } }7.2 对接云原生设施Kubernetes健康检查实现WebServlet(/health) public class HealthCheckServlet extends BaseServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { boolean healthy checkDatabase() checkCache(); resp.setStatus(healthy ? 200 : 503); resp.getWriter().write(healthy ? UP : DOWN); } private boolean checkDatabase() { try (Connection conn DbUtil.getConnection()) { return conn.isValid(1000); } catch (SQLException e) { return false; } } }这种纯Servlet方案特别适合需要精细控制资源的场景。我在一个物联网网关项目中采用该方案后不仅解决了Spring Boot内存泄漏问题还使95%的API响应时间控制在10ms以内。当然这种开发方式要求团队对Servlet规范有深入理解建议先在小规模项目中验证可行性。