轻量级仿 SpringBoot 程序

但凡 Java 程序,想必就是 Spring 程序;但凡 Spring 程序,想必就是 SpringBoot 程序——且慢,当今尚有不是 SpringBoot 即 SpringMVC 的程序不?有——老旧的遗留系统不就是嘛~——不,其实只要稍加“调教”,SpringMVC 即可原地变为非常近似 SpringBoot 的形态,——不信?且待笔者慢慢道来。

早在若干年前笔者已经热衷于无 XML 文件配置 SpringMVC,全部用注解来配置(参见博文)。不过仍然依赖独立 Tomcat 来运行。后来笔者更进一步,成功使用嵌入式的 Tomcat 来运行,使之形态更相似 SpringBoot,参见博文《轻量级仿 Spring Boot=嵌入式 Tomcat+Spring MVC》。当前方案在多个现实中项目中使用,可以说跟 SpringBoot 非常相似了。最近笔者比较有时间,于是重构了这个技术点,独立发布一个工程在 Github 上,并发布 Maven 中央库,同时也想回顾一下个中的原理。

这个技术点取名 lightweight-springboot,开源,可通过 Maven 依赖到你的项目中启动,与 SpringBoot 用法大体一致。

安装与使用

安装 lightweight-springboot

源码:https://github.com/lightweight-component/aj-lightweight-springboot。
Java Documents: https://dev.ajaxjs.com/docs/javadoc/aj-lightweight-springboot/。

需要 Java 1.8+, Maven:

<dependency><groupId>com.ajaxjs</groupId><artifactId>aj-lightweight-springboot</artifactId><version>1.1</version>
</dependency>

使用方式

在资源目录中安排 application.yml 文件,内容如下:

server:port: 8888 # 端口号context-path: /foo  # 项目名,如果不设定,默认是 /localFileUpload: true # 是否支持本地文件上传

代码结构按照惯常开发的模式即可。必须要有启动类和相关的配置类。

在这里插入图片描述

启动的main()函数内的start()必须传入配置类参数;指定@ComponentScan扫描包的范围。

import com.ajaxjs.framework.embeded_tomcat.BaseWebMvcConfigure;
import com.ajaxjs.framework.embeded_tomcat.EmbeddedTomcatStarter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;@Configuration
@EnableWebMvc
@ComponentScan({"com.ajaxjs.base", "com.ajaxjs.framework.data_service"})
public class BaseApplication extends BaseWebMvcConfigure {public static void main(String[] args) {EmbeddedTomcatStarter.start(BaseApplication.class); // BaseApplication 为配置类}
}

BaseApplication 配置类,配置数据库、注入依赖组件等等,如下例。

import com.ajaxjs.data.jdbc_helper.JdbcConn;
import com.ajaxjs.data.jdbc_helper.JdbcWriter;
import com.ajaxjs.iam.resource_server.UserInterceptor;
import com.ajaxjs.util.logger.LogHelper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.sql.DataSource;/*** 程序配置*/
@Configuration
public class BaseConfiguration implements WebMvcConfigurer {@Value("${db.url}")private String url;@Value("${db.user}")private String user;@Value("${db.psw}")private String psw;@Bean(value = "dataSource", destroyMethod = "close")DataSource getDs() {return JdbcConn.setupJdbcPool("com.mysql.cj.jdbc.Driver", url, user, psw);}
}

Demo 程序

为方便大家观摩了解,可以第一时间领略该项目,笔者准备了一个现成的演示程序,也可以作为创建程序的样板或者说“脚手架(Scaffolding)”,开发者可以在此基础上进行进一步的开发,而不必从头开始编写所有的代码。

https://github.com/lightweight-component/aj-framework-demo-service

启动流程

加载配置

当执行 Java 程序的main()函数时候,lightweight-springboot 会执行什么呢?首先是加载 yaml 配置文件,参见EmbeddedTomcatStarter类下面的start()方法。
在这里插入图片描述
解析 yaml 借助第三方组件 Snakeyaml,它手动加载配置的方法如下:

/*** 从类路径下的 application.yml 文件中获取服务器配置。** @return 服务器配置的 Map 对象,如果文件不存在或读取失败,则返回null*/
@SuppressWarnings("unchecked")
static Map<String, Object> getServerConfig() {ClassPathResource resource = new ClassPathResource("application.yml");if (!resource.exists())return null;try {Map<String, Object> yamlMap = new Yaml().load(resource.getInputStream());return (Map<String, Object>) yamlMap.get("server");} catch (IOException e) {throw new RuntimeException(e);}
} 

配置都返回为 Map,这就非常好用了。

初始化 Tomcat 对象

得到配置后就创建EmbeddedTomcatStarter实例并执行start(),包含以下流程:

initTomcat();
initConnector();
initContext();
runTomcat();

主要围绕 Tomcat 进行配置和启动。这里和 SpringBoot 不同:SpringBootmain()函数执行后,是先启动 Spring 自身的各项功能,几乎最后才启动 Web 容器的(如 Tomcat)。lightweight-springboot 则是先启动 Tomcat 再通知 Spring 初始化,与原来的 SpringMVC 启动方式差不多,也就是把原来webx.xml里面的配置,改为 Java 语言命令语句(Programmatically)去调用。当然,你必须了解 Tomcat 的 API 怎么调用才行。

Web 容器的一般配置,无非端口、ContextPath、BaseDir 诸如此类的配置,我们安排了一个 POJO TomcatConfig来管理这些配置。
在这里插入图片描述

Tomcat Connector

另外是对 Tomcat Connector 的配置。Tomcat Connector 是 Tomcat 服务器中负责接收请求、处理协议、与 Servlet 容器交互的桥梁,是 Tomcat 架构中的重要组成部分。通过合理配置和优化 Connector,可以改善 Web 程序并发、线程等的问题。另外还有用于监控的 JMX 也是通过 Connector 来配置的。

Tomcat Context

接着对 Tomcat Context 的配置。Tomcat Context 是 Tomcat 服务器中用于定义特定 Web 应用程序的环境和配置的组件,为 Web 应用提供了一个隔离的环境,使得不同的 Web 应用可以在同一台服务器上独立运行,互不干扰。通过合理配置 Context,可以优化 Web 应用的性能和安全性。不过我们的程序一个 jar 包就是一个 context,不存在多个 context。

比较相关的是设置 JSP 页面。一般来讲此类的工程都不是为 Web 页面服务的,都是做 API 接口的,故所以一般不需要 JSP 表示层渲染,而且禁止 JSP 及其他资源的扫描(Servlet 3 的新特性),可以大大加快程序的启动。

启动 Tomcat

最后,执行runTomcat()启动 Tomcat。

启动 Spring

咦~上述过程怎么没有提及如何启动 Spring 的?稍安勿躁,这不就讲讲如何启动 Spring,它是专门规划到一个类EmbeddedTomcatStarter,上述的流程只是针对 Tomcat 的,是为TomcatStarter类,然后EmbeddedTomcatStarter继承于TomcatStarter,——这样规划比较清晰。

我们看看EmbeddedTomcatStarter.onContextReady()源码在这里插入图片描述

可见是在 Context 就绪之后才能初始化 Spring。之前笔者尝试不用异步/事件的方法调用,但结构上不太理想,还是按照 Tomcat API 标准的写法去弄。

注意这里的初始化 Spring 语句,其实与web.xml的非常像,就是换了个写法。这时org.springframework.web.context.support.AnnotationConfigWebApplicationContext会创建一个ApplicationContext,这是 Spring 应用程序的核心,用于管理所有的 Spring 组件。

此时,Spring 就进行其自身的相关初始化工作:首先遍历所有的 Bean 定义,包括 @Component@Service@Repository@Controller 等注解的类,并将它们注册到 Spring 容器中;然后根据 Bean 定义创建 Bean 实例,并进行依赖注入;如果 Bean 实现了InitializingBean接口或通过@PostConstruct注解标记的方法,Spring 将调用这些方法进行初始化。最后注册所有的事件监听器,并在ApplicationContext初始化完成后触发ApplicationEvent

// 配置 ServletContext 参数,指定使用的上下文类。
ctx.setInitParameter("contextClass", "org.springframework.web.context.support.AnnotationConfigWebApplicationContext");
ctx.addListener(new ContextLoaderListener()); // 添加 ContextLoaderListener 监听器,用于初始化和销毁 Spring Web 上下文
ctx.setAttribute("ctx", ctx.getContextPath()); // 设置上下文路径到 ServletContext 属性,以便在 JSP 中使用// 绑定 Servlet,配置 Spring MVC 的 DispatcherServlet,并设置其加载优先级
ServletRegistration.Dynamic registration = ctx.addServlet("dispatcher", new DispatcherServlet(ac));
registration.setLoadOnStartup(1);// 设置 Tomcat 启动立即加载 Servlet
registration.addMapping("/"); // 浏览器访问 uri。注意不要设置 /*// 配置字符过滤器,确保请求和响应的字符编码正确
FilterRegistration.Dynamic filterReg = ctx.addFilter("InitMvcRequest", new UTF8CharsetFilter());
filterReg.addMappingForUrlPatterns(null, true, "/*");

回到启动这里,值得一提的是下一步的绑定 Servlet:配置 SpringMVC 的 DispatcherServlet,并设置其加载优先级。我们知道,SpringMVC 是通过 DispatcherServlet 来分发处理请求,在 SpringBoot 出现之前,都是需要在web.xml里配置,来实现请求的拦截。

而在 Servlet 3.0 之后,规则中新增了 Dynamic Servlet、Dynamic Filter 这些概念, 可以在运行时动态添加/注册组件到 Context 中。这里就是 Dynamic Servlet、Dynamic Filter 的应用。

ServletContainerInitializer

上述过程是使用 Java 语言“硬编码”调用 Spring 的,其实还有一种方法只要配置不用编码就可以启动 Spring,那就是 SCI(ServletContainerInitializer)的应用。SCI 不是 Spring 的特性,而是 Servlet 3.0 标准的特性,用于接收 Web 应用在启动阶段通知的接口,再根据通知进行一些编程式的处理,比如触发 Spring 启动、动态注册Servlet、Filter 等。

也就是说,当你希望 WebApp 启动时候,也启动某些组件,就可以使用这个 SCI。组件具体可以是你的 jar 包。容器在启动应用的时候,会扫描当前应用每一个 jar 包里面META-INF/services/javax.servlet.ServletContainerInitializer指定的实现类,启动并运行这个实现类的onStartup方法。这种称为可插拔性(Pluggability)。

Jar 文件的 META-INF 中 services 中包含一个 SCI 的声明。在这里插入图片描述
SpringBoot 也是这样被点燃的

public void onStartup(ServletContext servletContext) throws ServletException {this.logger = LogFactory.getLog(this.getClass());WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);if(rootAppContext != null) {servletContext.addListener(new ContextLoaderListener(rootAppContext) {public void contextInitialized(ServletContextEvent event) {}});} else {this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context");}}

参考文章:《Tomcat 是怎样处理 SpringBoot 应用的?》、《Tomcat 中的可插拔以及 SCI 的实现原理》。

SPI 机制

顺带说说与 SCI 相仿的 SPI。我们知道 SpringBoot 相较于 Spring 的一大特性就是自动装配,那么自动装配是怎么具体实现的呢?其实在实现自动装配上 SpringBoot 采用了多种方案结合的,比如基于 Spring 的扩展点的自动属性注入等,还有提供了一套 SPI 机制让程序自动可插拔的装配。

SPI(Service Provider Interface)是 JDK 内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,它允许开发者编写一个服务接口,然后通过在项目中使用服务提供者实现该接口的方式,实现对应的服务功能。

SPI 机制通过在 Classpath 中的META-INF/services目录下,创建以服务接口全限定名命名的文件,文件的内容为实现该接口的具体实现类。当应用程序需要使用该服务时,JDK 会自动加载并实例化配置文件中列出的实现类,并提供给应用程序使用。

参见《深入剖析 Spring Boot 的 SPI 机制 》、《SpringBoot(二):SpringBoot自动装配之 SPI 机制》。

扩展

单纯启动 Tomcat/Spring 没几个类,lightweight-springboot 核心就这几个类:
在这里插入图片描述
filter 包下面的是扩展的过滤器,或者说拦截器。当前默认加载的有:

  • UTF8CharsetFilter,避免中文乱码
  • FileUploadHelper,Servlet 3 自带的文件上传功能
  • ShowControllerInterceptor拦截器,获得 Controller 方法名、请求参数和注解信息,打印出来,以方便调试。这个要 Spring 配置来启动。

lightweight-springboot 作为提供底层功能的库,更多的扩展或配置是放在上层去调用的。我们知道,SpringBoot 一般是实现WebMvcConfigurer接口来配置的,——在 lightweight-springboot 同样如是。比如说配置统一返回 JSON 格式、全局异常拦截、相关通用 Bean 注入的等等。这属于上层框架配置的职责,每一个项目可能都不一样,就没有放在 lightweight-springboot 了。

具体可以看看笔者框架的实现:BaseWebMvcConfigure https://github.com/lightweight-component/aj-framework/blob/main/src/main/java/com/ajaxjs/framework/BaseWebMvcConfigure.java。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/861662.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

TikTok网页版使用指南:如何登录TikTok网页版?

海外版抖音TikTok&#xff0c;已成为连接全球观众的重要平台。据统计&#xff0c;在美国&#xff0c;TikTok的用户数量已达到近1.3亿&#xff0c;并且在国外的95后用户群体中很受欢迎。 TikTok网页版也提供了一个广阔的平台&#xff0c;让品牌和创作者在电脑端与全球观众互动&…

智能语音抽油烟机:置入WTK6900L离线语音识别芯片 掌控厨房新风尚

一、抽油烟机语音识别芯片开发背景 在繁忙的现代生活中&#xff0c;人们对于家居生活的便捷性和舒适性要求越来越高。传统的抽油烟机操作方式往往需要用户手动调节风速、开关等功能&#xff0c;不仅操作繁琐&#xff0c;而且在烹饪过程中容易分散注意力&#xff0c;增加安全隐…

单点登录方法

一、父域cookie:两个有相同父域名的二级域名之间可以跨域传递cookie //注意该接口的地址也是baidu.com下属的二级域名:a.baidu.com //全部接口地址为:a.baidu.com/dev-api/system/ecdWeb/login。如果不是a.baidu.com那么根本带不过去 //其实可以理解为通过该方法将cookie传给…

获取股票列表关键信息

获取股票列表的关键信息通常包括以下几个方面: 1. **股票代码**:股票的唯一标识符,通常由字母和数字组成,如"AAPL"代表苹果公司的股票。 2. **公司名称**:股票所代表的公司全称。 3. **行业板块**:股票所属的行业领域,如科技、金融、医疗等。 4. **市场类…

大数据处理引擎选型之 Hadoop vs Spark vs Flink

随着大数据时代的到来&#xff0c;处理海量数据成为了各个领域的关键挑战之一。为了应对这一挑战&#xff0c;多个大数据处理框架被开发出来&#xff0c;其中最知名的包括Hadoop、Spark和Flink。本文将对这三个大数据处理框架进行比较&#xff0c;以及在不同场景下的选择考虑。…

Linux内存管理(七十三):cgroup v2 简介

版本基于: Linux-6.6 约定: 芯片架构:ARM64内存架构:UMACONFIG_ARM64_VA_BITS:39CONFIG_ARM64_PAGE_SHIFT:12CONFIG_PGTABLE_LEVELS :31. cgroup 简介 术语: cgroup:control group 的缩写,永不大写(never capitalized); 单数形式的 cgroup 用于指定整个特性,也用…

ubuntu篇---添加环境变量并且在pycharm中使用

ubuntu篇—添加环境变量并且在pycharm中使用 一. 添加环境变量 vim ~/.bashrc 在文件末尾加上 保存退出 source ~/.bashrc二. 在pycharm中添加环境变量 1.打开pycharm&#xff0c;并打开你的项目 2.点击菜单栏中的“Run”&#xff0c; 选择“Edit Configurations” 3.在弹…

pytorch为自己的extension backend添加profiler功能

pytorch为自己的extension backend添加profiler功能 1.参考文档2.your-extension-for-pytorch需要增加的代码3.pytorch demo及如何调整chrome trace json文件4.[可视化](https://ui.perfetto.dev/) 本文演示了pytorch如何为自己的extension backend添加profiler功能 背景介绍 …

Taro +vue3 中的微信小程序中的分享

微信小程序 右上角分享 的触发 以及配 useShareAppMessage(() > {return {title: "电影属全国通兑券",page: /pages/home/index,imageUrl: "http:///chuanshuo.jpg",};}); 置 就是Taro框架中提供的一个分享Api 封装好的

Tailwind CSS 在vue里 的使用

Tailwind CSS 在vue里 的使用 安装 npm install -D tailwindcsslatest postcsslatest autoprefixerlatest创建您的配置文件 生成您的 tailwind.config.js 和 postcss.config.js 文件&#xff1a; npx tailwindcss init -p您的项目根目录创建一个最小化的 tailwind.config.js…

项目经理必读:三步走实现项目高效管理

一个项目的成功往往取决于项目管理能力的高低。若管理不当&#xff0c;易导致团队成员间的推诿和抱怨&#xff0c;且项目团队还可能面临成员对目标不明确、信息不透明、进度难以跟踪等问题。作为项目经理&#xff0c;掌握有效的项目管理策略至关重要。 一、精细化的目标拆解 …

数据库逻辑结构设计-实体和实体间联系的转换、关系模式的优化

一、引言 如何将数据库概念结构设计的结果&#xff0c;即用E-R模型表示的概念模型转化为关系数据库模式。 E-R模型由实体、属性以及实体间的联系三个要素组成 将E-R模型转换为关系数据库模式&#xff0c;实际上就是要将实体及实体联系转换为相应的关系模式&#xff0c;转换…

模板特化的作用是什么

模板特化的作用是在某种特定类型下为模板提供具体的实现&#xff0c;以解决通用模板在某些特殊类型下无法满足特定需求的问题。模板特化分为全特化和偏特化两种形式。 1. 全特化&#xff08;全具体化&#xff09; 定义&#xff1a;全特化是当模板的所有模板参数都明确指定了具…

「树莓派入门」树莓派基础03-DRP远程连接控制树莓派(无线网络连接)

一、无线网络连接配置 1. 确认无线网络设备状态 使用 iwconfig 命令来查看无线网络接口的状态。 iwconfig2. 扫描无线网络 使用 iwlist 命令扫描可用的无线网络。 sudo iwlist wlan0 scan3. 配置无线网络连接 创建或编辑 wpa_supplicant.conf 文件&#xff0c;配置无线网…

【EtherCAT】TwinCAT3通过PLC修改SDO数据

目录 1、打开twincat3, 左边PLC右键->添加新项&#xff0c;建立PLC工程 2、->References右键添加库 3、找到Tc2_EtherCAT库&#xff0c;点确定。 4、PLC程序ST语言就可以调用下面的功能块函数 5、PLC编程界面右键->输入助手 1、打开twincat3, 左边PLC右键->添…

数据恢复篇:如何恢复丢失的Android短信?

许多用户发现自己处于重要短信意外从Android手机中删除的情况。幸运的是&#xff0c;有一些行之有效的方法可以在没有root的情况下恢复已删除的短信Android&#xff0c;这可以成为救命稻草。这些技术不需要深厚的技术知识&#xff0c;也不需要损害设备的安全性。为了帮助您摆脱…

DDei在线设计器-API-DDeiAbstractShape

DDeiAbstractShape DDeiAbstractShape代表是所有可见图形的父类&#xff0c;定义了图形所需要的公共属性和方法。   DDeiAbstractShape实例包含了一个图形的所有数据和渲染器&#xff0c;在获取后可以通过它访问其他内容。DDeiAbstractShape中的layer指向所在图层,stage指向所…

使用 Node.js 发送电子邮件

前言&#xff1a;大多数 Web 应用程序都需要发送电子邮件。它可能用于注册、密码重置、状态报告&#xff0c;甚至是完整的营销活动&#xff08;例如新闻通讯和促销&#xff09;。本教程介绍如何在 Node.js 中发送电子邮件&#xff0c;但这些概念和挑战适用于您使用的任何系统。…

Spring Boot与JMS消息中间件的集成

Spring Boot与JMS消息中间件的集成 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们来探讨一下如何在Spring Boot中集成JMS&#xff08;Java Message Se…

jieba--《红楼梦》章节分卷并计算TF-IDF值(超详细)

目录 大致步骤&#xff1a; 任务1&#xff1a; 将红楼梦 根据卷名 分隔成 卷文件 红楼梦txt&#xff1a; 红楼梦卷头&#xff1a; 红楼梦章节分卷&#xff1a; 任务2&#xff1a;对每个卷进行分词&#xff0c;并删除包含停用词的内容 1.遍历所有卷的内容&#xff0c;并添…