refactored, pre-buffer reuse
This commit is contained in:
@@ -37,3 +37,5 @@ build/
|
|||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
CLAUDE.md
|
||||||
Generated
-10
@@ -1,10 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Ignored default folder with query files
|
|
||||||
/queries/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
Generated
+2
@@ -3,6 +3,8 @@
|
|||||||
<component name="Encoding">
|
<component name="Encoding">
|
||||||
<file url="file://$PROJECT_DIR$/flash-bench/src/main/java" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-bench/src/main/java" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/flash-bench/src/main/resources" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/flash-bench/src/main/resources" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/flash/src/main/java" charset="UTF-8" />
|
||||||
|
<file url="file://$PROJECT_DIR$/flash/src/main/resources" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
Generated
+1
-5
@@ -10,12 +10,8 @@
|
|||||||
<option name="originalFiles">
|
<option name="originalFiles">
|
||||||
<list>
|
<list>
|
||||||
<option value="$PROJECT_DIR$/pom.xml" />
|
<option value="$PROJECT_DIR$/pom.xml" />
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
<option name="ignoredFiles">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$/flash-bench/pom.xml" />
|
<option value="$PROJECT_DIR$/flash-bench/pom.xml" />
|
||||||
</set>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
Generated
+290
@@ -0,0 +1,290 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="pre-module refactor">
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/flash-bench/pom.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/flash-bench/src/main/java/dev/relism/bench/Main.java" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/flash-bench/src/main/resources/simplelogger.properties" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/flash/pom.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/encodings.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/encodings.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/CLAUDE.md" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/pom.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/Flash.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Flash.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/HttpServer.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/HttpServerConfiguration.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServerConfiguration.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/Main.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Main.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/RequestParser.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/RequestParser.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/exceptions/DuplicateNamespaceException.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/http/ContentType.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/ContentType.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/http/HttpMethod.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpMethod.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/http/HttpStatus.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpStatus.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/HeaderMap.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/HeaderMap.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/LazyBody.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/LazyBody.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/PathParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/PathParams.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/QueryParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/Request.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Request.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/RequestHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestHandler.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/RequestLine.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestLine.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/Response.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Response.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/models/SimpleHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/SimpleHandler.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/AbstractRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/GlobalRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/GlobalRouter.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/PathUtils.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/PathUtils.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/Route.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/Route.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/radix/RadixPathRouterImpl.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/template/ByteTemplate.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/template/ByteTemplate.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/java/dev/relism/template/ErrorPages.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/template/ErrorPages.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/resources/assets/html/default_404.html" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_404.html" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/resources/assets/html/default_exception.html" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_exception.html" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/main/resources/assets/logo.png" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/logo.png" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/HttpServerConcurrencyTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerConcurrencyTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/HttpServerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/RequestParserTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/RequestParserTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/http/ContentTypeTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/ContentTypeTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/http/HttpMethodTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpMethodTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/http/HttpStatusTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/http/HttpStatusTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/HeaderMapTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/HeaderMapTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/PathParamsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/PathParamsTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/QueryParamsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/QueryParamsTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/RequestLineTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestLineTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/RequestTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/ResponseTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/ResponseTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/models/SimpleHandlerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/SimpleHandlerTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/AbstractRouterTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/AbstractRouterTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/GlobalRouterTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/GlobalRouterTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/PathUtilsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/PathUtilsTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathViewsTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/template/ByteTemplateTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ByteTemplateTest.java" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/test/java/dev/relism/template/ErrorPagesTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ErrorPagesTest.java" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="CopilotPersistence">
|
||||||
|
<persistenceIdMap>
|
||||||
|
<entry key="_C:/Users/elorc/Documents/Coding/Java/practice/Flash" value="3Axc6mzLyNvh4TgGFvNSCSq83xw" />
|
||||||
|
<entry key="_C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice" value="3AoiAx4zfxMcuO6fDI9oALOPcq1" />
|
||||||
|
</persistenceIdMap>
|
||||||
|
</component>
|
||||||
|
<component name="EmbeddingIndexingInfo">
|
||||||
|
<option name="cachedIndexableFilesCount" value="88" />
|
||||||
|
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="FileTemplateManagerImpl">
|
||||||
|
<option name="RECENT_TEMPLATES">
|
||||||
|
<list>
|
||||||
|
<option value="Enum" />
|
||||||
|
<option value="Class" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="GitHubPullRequestSearchHistory">{
|
||||||
|
"lastFilter": {
|
||||||
|
"state": "OPEN",
|
||||||
|
"assignee": "Relism"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="GithubPullRequestsUISettings">{
|
||||||
|
"selectedUrlAndAccountId": {
|
||||||
|
"url": "https://github.com/Relism/Flash5.git",
|
||||||
|
"accountId": "86d8a39c-af27-4b79-8d5d-dc74375c3348"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="McpProjectServerCommands">
|
||||||
|
<commands />
|
||||||
|
<urls />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"associatedIndex": 3
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="3AoiAx4zfxMcuO6fDI9oALOPcq1" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">{
|
||||||
|
"keyToString": {
|
||||||
|
"Application.ExternalBenchmark.executor": "Run",
|
||||||
|
"Application.Main.executor": "Run",
|
||||||
|
"Application.dev.relism.bench.Main.executor": "Run",
|
||||||
|
"JUnit.RequestParserTest.executor": "Run",
|
||||||
|
"JUnit.RequestParserTest.headers_caseInsensitive.executor": "Debug",
|
||||||
|
"Maven.FlashPractice [test].executor": "Run",
|
||||||
|
"Maven.flash [compile].executor": "Run",
|
||||||
|
"Maven.flash [install].executor": "Run",
|
||||||
|
"Maven.flash-bench [clean].executor": "Run",
|
||||||
|
"Maven.flash-bench [install].executor": "Run",
|
||||||
|
"Maven.flash-bench [validate].executor": "Run",
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RunOnceActivity.MCP Project settings loaded": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
|
"git-widget-placeholder": "master",
|
||||||
|
"ignore.virus.scanning.warn.message": "true",
|
||||||
|
"kotlin-language-version-configured": "true",
|
||||||
|
"last_opened_file_path": "C:/Users/elorc/Documents/Coding/Java/practice/Flash",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"onboarding.tips.debug.path": "C:/Users/elorc/Documents/Coding/Java/practice/FlashPractice/flash-bench/src/main/java/dev/relism/Main.java",
|
||||||
|
"project.structure.last.edited": "Modules",
|
||||||
|
"project.structure.proportion": "0.15",
|
||||||
|
"project.structure.side.proportion": "0.1150748",
|
||||||
|
"settings.editor.selected.configurable": "ml.llm.LLMThirdPartyAiProvidersConfigurable",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="RecentsManager">
|
||||||
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="C:\Users\elorc\Documents\Coding\Java\practice\Flash" />
|
||||||
|
</key>
|
||||||
|
</component>
|
||||||
|
<component name="RunManager" selected="Application.ExternalBenchmark">
|
||||||
|
<configuration name="ExternalBenchmark" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
|
||||||
|
<option name="MAIN_CLASS_NAME" value="dev.relism.bench.ExternalBenchmark" />
|
||||||
|
<module name="flash-bench" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="dev.relism.bench.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
<configuration name="Main" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
|
||||||
|
<option name="MAIN_CLASS_NAME" value="dev.relism.Main" />
|
||||||
|
<module name="flash" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="dev.relism.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
<configuration name="dev.relism.bench.Main" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
|
||||||
|
<option name="MAIN_CLASS_NAME" value="dev.relism.bench.Main" />
|
||||||
|
<module name="flash-bench" />
|
||||||
|
<extension name="coverage">
|
||||||
|
<pattern>
|
||||||
|
<option name="PATTERN" value="dev.relism.bench.*" />
|
||||||
|
<option name="ENABLED" value="true" />
|
||||||
|
</pattern>
|
||||||
|
</extension>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
<recent_temporary>
|
||||||
|
<list>
|
||||||
|
<item itemvalue="Application.ExternalBenchmark" />
|
||||||
|
<item itemvalue="Application.dev.relism.bench.Main" />
|
||||||
|
<item itemvalue="Application.Main" />
|
||||||
|
</list>
|
||||||
|
</recent_temporary>
|
||||||
|
</component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-jdk-30f59d01ecdd-2fc7cc6b9a17-intellij.indexing.shared.core-IU-253.30387.90" />
|
||||||
|
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-IU-253.30387.90" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="" />
|
||||||
|
<created>1773265186049</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1773265186049</updated>
|
||||||
|
<workItem from="1773265187409" duration="5496000" />
|
||||||
|
<workItem from="1773313172181" duration="9617000" />
|
||||||
|
<workItem from="1773325837582" duration="3918000" />
|
||||||
|
<workItem from="1773400995673" duration="23805000" />
|
||||||
|
<workItem from="1773442006686" duration="2722000" />
|
||||||
|
<workItem from="1773491408276" duration="23095000" />
|
||||||
|
<workItem from="1773528595525" duration="7400000" />
|
||||||
|
<workItem from="1773536222450" duration="680000" />
|
||||||
|
<workItem from="1773536904928" duration="395000" />
|
||||||
|
<workItem from="1773537302622" duration="150000" />
|
||||||
|
<workItem from="1773537489622" duration="5048000" />
|
||||||
|
<workItem from="1773576241900" duration="5173000" />
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00001" summary="Initial">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773536078762</created>
|
||||||
|
<option name="number" value="00001" />
|
||||||
|
<option name="presentableId" value="LOCAL-00001" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773536078762</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00002" summary="pre-module refactor">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1773536678416</created>
|
||||||
|
<option name="number" value="00002" />
|
||||||
|
<option name="presentableId" value="LOCAL-00002" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1773536678416</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="3" />
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
<component name="Vcs.Log.Tabs.Properties">
|
||||||
|
<option name="TAB_STATES">
|
||||||
|
<map>
|
||||||
|
<entry key="MAIN">
|
||||||
|
<value>
|
||||||
|
<State />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="VcsManagerConfiguration">
|
||||||
|
<MESSAGE value="Initial" />
|
||||||
|
<MESSAGE value="pre-module refactor" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="pre-module refactor" />
|
||||||
|
</component>
|
||||||
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
|
<expand />
|
||||||
|
<select />
|
||||||
|
</component>
|
||||||
|
<component name="github-copilot-workspace">
|
||||||
|
<instructionFileLocations>
|
||||||
|
<option value=".github/instructions" />
|
||||||
|
</instructionFileLocations>
|
||||||
|
<promptFileLocations>
|
||||||
|
<option value=".github/prompts" />
|
||||||
|
</promptFileLocations>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Build & Run Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the project
|
|
||||||
mvn compile
|
|
||||||
|
|
||||||
# Package (JAR)
|
|
||||||
mvn package
|
|
||||||
|
|
||||||
# Run (entry point is dev.relism.Main)
|
|
||||||
mvn exec:java -Dexec.mainClass="dev.relism.Main"
|
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
mvn clean
|
|
||||||
|
|
||||||
# Full clean build
|
|
||||||
mvn clean package
|
|
||||||
```
|
|
||||||
|
|
||||||
No tests exist yet. The project resolves the `fpr-core` dependency from a private Reposilite repository at `https://maven.relism.dev/releases`.
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
**Flash** is a hand-rolled, zero-allocation HTTP/1.1 server built on raw Java sockets with virtual threads (Java 21). The central design goal is extreme low latency — no intermediate String allocations on the hot path.
|
|
||||||
|
|
||||||
### Request Lifecycle
|
|
||||||
|
|
||||||
```
|
|
||||||
Socket → RequestParser → GlobalRouter → AbstractRouter impl → RequestHandler → Response bytes → Socket
|
|
||||||
```
|
|
||||||
|
|
||||||
1. **`RequestParser`** — Reads raw bytes from the socket into an 8 KB buffer and scans for method, path, headers, and body without creating intermediate String objects. All parsed values are wrapped in `ByteView` implementations (see `FastPathViews`).
|
|
||||||
|
|
||||||
2. **`GlobalRouter`** — The top-level dispatcher. Holds a sorted list of mounted sub-routers (longest namespace prefix first) and a default internal router. Dispatches each request via byte-level prefix matching (`ByteUtils.startsWith`).
|
|
||||||
|
|
||||||
3. **`FastPathRouterImpl`** — The primary router implementation, backed by the external `fpr-core` library. Builds a compiled state machine (`RouterBuilder → FastPathRouter`) for ultra-fast byte-level method+path matching. Routes are marked dirty (`router = null`) when added and lazily recompiled on first request. Thread-local `MatchResult` objects avoid per-request allocation.
|
|
||||||
|
|
||||||
4. **`RadixPathRouterImpl`** — Stub; not yet implemented.
|
|
||||||
|
|
||||||
### Key Abstractions
|
|
||||||
|
|
||||||
- **`AbstractRouter`** — Base class for all routers. Has a `namespace` (byte array for zero-alloc prefix checks) and two abstract methods: `route(Request)` and `addRoute(HttpMethod, String, RequestHandler)`.
|
|
||||||
- **`RequestHandler`** — Abstract class handlers extend. The `handle(Request, Response)` method returns either a `Response` object or any other value (serialized as the body) or `null`.
|
|
||||||
- **`SimpleHandler`** — Wraps a `FunctionalHandler` lambda for the fluent DSL (`server.get(path, handler)`).
|
|
||||||
- **`@Route` annotation** — Applied to `RequestHandler` subclasses to declare their HTTP method and path. Used by `AbstractRouter.register()`.
|
|
||||||
- **`ByteView`** (`fpr-core` interface) — Zero-copy abstraction over byte sequences. Implemented by `RequestByteView` (slice into the raw buffer), `MethodPathByteView` (virtual concatenation of method + path), `SocketByteView`, and `StringByteView` — all in `FastPathViews`.
|
|
||||||
|
|
||||||
### Routing Registration (Two Styles)
|
|
||||||
|
|
||||||
**Annotation-based (class handlers):**
|
|
||||||
```java
|
|
||||||
@Route(method = "GET", path = "/profile")
|
|
||||||
public static class ProfileHandler extends RequestHandler { ... }
|
|
||||||
|
|
||||||
apiRouter.register(new ProfileHandler()); // namespace "/api" + path "/profile" = "/api/profile"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fluent DSL (lambdas):**
|
|
||||||
```java
|
|
||||||
server.get("/hello", (req, res) -> "Hello World");
|
|
||||||
```
|
|
||||||
|
|
||||||
Paths registered via `addRoute` are prefixed with the router's namespace inside `FastPathRouterImpl.addRoute`.
|
|
||||||
|
|
||||||
### Response Writing
|
|
||||||
|
|
||||||
`HttpServer` writes HTTP/1.1 responses with pre-calculated static byte arrays (`HTTP_1_1`, `CRLF`, `CONTENT_TYPE`, `CONTENT_LENGTH`) to avoid per-response allocations. `IoUtils.writeInt` writes integer values without String conversion.
|
|
||||||
|
|
||||||
### Performance Invariants to Maintain
|
|
||||||
|
|
||||||
- No `String` creation on the routing hot path (use `ByteView` and byte arrays).
|
|
||||||
- Thread-local `MatchResult` reuse in `FastPathRouterContext` — do not allocate per request.
|
|
||||||
- `GlobalRouter.route` must stay allocation-free (sorted list iterated directly).
|
|
||||||
- `RequestParser` assumes headers fit in one 8 KB burst read.
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Flash Benchmarking Module
|
||||||
|
|
||||||
|
This module is designed for high-concurrency benchmarks using `wrk`.
|
||||||
|
|
||||||
|
## Prerequisite
|
||||||
|
- Java 21+
|
||||||
|
- `wrk` installed (`sudo apt install wrk` on Linux/WSL)
|
||||||
|
|
||||||
|
## Running the benchmarks
|
||||||
|
|
||||||
|
1. **Build the project** (from the root directory):
|
||||||
|
```bash
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start the benchmark server**:
|
||||||
|
```bash
|
||||||
|
java -jar flash-bench/target/flash-bench-1.0-SNAPSHOT.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Execute the benchmark suite** (from a new terminal):
|
||||||
|
```bash
|
||||||
|
cd flash-bench
|
||||||
|
./benchmark.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced: Random IDs Benchmarking
|
||||||
|
To test the speed of parameter extraction across millions of unique IDs:
|
||||||
|
```bash
|
||||||
|
wrk -t12 -c500 -d30s -s pipeline.lua http://127.0.0.1:8080
|
||||||
|
```
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>flash-parent</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>flash-bench</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<jmh.version>1.37</jmh.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>flash</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.3</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals><goal>shade</goal></goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>dev.relism.bench.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
<!-- Required by JMH to merge service-loader files -->
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package dev.relism.bench;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.results.format.ResultFormatType;
|
||||||
|
import org.openjdk.jmh.runner.Runner;
|
||||||
|
import org.openjdk.jmh.runner.options.Options;
|
||||||
|
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
if (args.length > 0 && args[0].equalsIgnoreCase("external")) {
|
||||||
|
ExternalBenchmark.main(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Starting JMH Benchmarks... (Pass 'external' as arg to start standalone server)");
|
||||||
|
Options opts = new OptionsBuilder()
|
||||||
|
.include(FlashBenchmark.class.getSimpleName())
|
||||||
|
.resultFormat(ResultFormatType.TEXT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
new Runner(opts).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Silence all logs during benchmarks to avoid I/O becoming the bottleneck
|
||||||
|
org.slf4j.simpleLogger.defaultLogLevel=off
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>flash-parent</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>flash</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.relism</groupId>
|
||||||
|
<artifactId>fpr-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
package dev.relism;
|
||||||
|
|
||||||
|
import dev.relism.fpr.core.ByteView;
|
||||||
|
import dev.relism.http.ContentType;
|
||||||
|
import dev.relism.http.HttpStatus;
|
||||||
|
import dev.relism.models.*;
|
||||||
|
import dev.relism.routing.AbstractRouter;
|
||||||
|
import dev.relism.routing.GlobalRouter;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the Flash HTTP server.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Internally, the server owns a {@link GlobalRouter} with two tiers of routing:
|
||||||
|
* <ol>
|
||||||
|
* <li><b>Mounted sub-routers</b> — registered via {@link #mount}. Each owns a
|
||||||
|
* namespace
|
||||||
|
* prefix (e.g. {@code /api}) and handles all paths under it. Matched by longest
|
||||||
|
* prefix first.</li>
|
||||||
|
* <li><b>Internal router</b> — the fallback used when no sub-router claims the
|
||||||
|
* path.
|
||||||
|
* Routes registered directly on the server ({@link #get}, {@link #post}, etc.)
|
||||||
|
* go here.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Error handlers ({@link #onNotFound}, {@link #onException}) registered on the
|
||||||
|
* server
|
||||||
|
* apply only to the internal router. Each mounted sub-router has its own
|
||||||
|
* independent handlers.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class HttpServer {
|
||||||
|
private final HttpServerConfiguration configuration;
|
||||||
|
private final ServerSocket serverSocket;
|
||||||
|
private final GlobalRouter globalRouter = new GlobalRouter();
|
||||||
|
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
private volatile boolean stopped = false;
|
||||||
|
private final CompletableFuture<Void> readyFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
private static final byte[] HTTP_1_1 = "HTTP/1.1 ".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CONTENT_TYPE = "Content-Type: ".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CONTENT_LENGTH = "Content-Length: ".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CONNECTION_CLOSE = "Connection: close\r\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] CONNECTION_KEEPALIVE = "Connection: keep-alive\r\n".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
private static final byte[] UNKNOWN_STATUS_SUFFIX = " Unknown".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
private static final byte[][] DIGITS = new byte[10][1];
|
||||||
|
static {
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
DIGITS[i] = String.valueOf(i).getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpServer(HttpServerConfiguration configuration) throws IOException {
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.serverSocket = new ServerSocket(configuration.getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server on a new non-daemon virtual thread and returns a future
|
||||||
|
* that completes
|
||||||
|
* when the accept loop is running and the server is ready to serve requests.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> start() {
|
||||||
|
Thread.ofPlatform().name("flash-accept-loop").daemon(false).start(this::run);
|
||||||
|
return readyFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void run() {
|
||||||
|
try (executorService) {
|
||||||
|
readyFuture.complete(null);
|
||||||
|
while (!stopped) {
|
||||||
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
process(clientSocket);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (!stopped) {
|
||||||
|
readyFuture.completeExceptionally(e);
|
||||||
|
log.error("Accept loop error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the server and waits for in-flight requests to complete.
|
||||||
|
* The returned future is already completed when this method returns.
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> stop() {
|
||||||
|
stopped = true;
|
||||||
|
try {
|
||||||
|
serverSocket.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error closing server socket", e);
|
||||||
|
}
|
||||||
|
executorService.shutdown();
|
||||||
|
try {
|
||||||
|
if (!executorService.awaitTermination(30, TimeUnit.SECONDS))
|
||||||
|
executorService.shutdownNow();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
executorService.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Routing ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mounts a sub-router under the given namespace prefix.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Any request whose path starts with {@code namespace} will be dispatched to
|
||||||
|
* {@code router}
|
||||||
|
* instead of the internal router. When multiple namespaces match, the longest
|
||||||
|
* prefix wins.
|
||||||
|
* The router's own {@code onNotFound} and {@code onException} handlers are used
|
||||||
|
* for its paths —
|
||||||
|
* the server-level handlers do not apply.
|
||||||
|
*
|
||||||
|
* @throws dev.relism.exceptions.DuplicateNamespaceException if
|
||||||
|
* {@code namespace}
|
||||||
|
* is already mounted
|
||||||
|
*/
|
||||||
|
public HttpServer mount(String namespace, AbstractRouter router) {
|
||||||
|
globalRouter.mount(namespace, router);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Global error handlers ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the 404 handler for routes registered directly on this server.
|
||||||
|
* Does not affect mounted sub-routers, which each carry their own not-found
|
||||||
|
* handler.
|
||||||
|
*/
|
||||||
|
public HttpServer onNotFound(SimpleHandler.FunctionalHandler handler) {
|
||||||
|
globalRouter.onNotFound(handler);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the exception handler for routes registered directly on this server.
|
||||||
|
* Does not affect mounted sub-routers, which each carry their own exception
|
||||||
|
* handler.
|
||||||
|
*/
|
||||||
|
public HttpServer onException(AbstractRouter.ExceptionHandler handler) {
|
||||||
|
globalRouter.onException(handler);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DSL ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an annotation-based handler on the internal router.
|
||||||
|
* The handler's class must carry a {@link dev.relism.routing.Route @Route}
|
||||||
|
* annotation
|
||||||
|
* declaring the HTTP method and path. To register on a specific sub-router,
|
||||||
|
* call
|
||||||
|
* {@link AbstractRouter#register} on that router directly.
|
||||||
|
*/
|
||||||
|
public HttpServer register(RequestHandler handler) {
|
||||||
|
globalRouter.register(handler);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a route on the internal router (not on any mounted sub-router).
|
||||||
|
* The handler's return value is used as the response body; returning a
|
||||||
|
* {@link dev.relism.models.Response}
|
||||||
|
* instance replaces the entire response object.
|
||||||
|
*/
|
||||||
|
public HttpServer get(String path, SimpleHandler.FunctionalHandler h) {
|
||||||
|
globalRouter.get(path, h);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @see #get(String, SimpleHandler.FunctionalHandler) */
|
||||||
|
public HttpServer post(String path, SimpleHandler.FunctionalHandler h) {
|
||||||
|
globalRouter.post(path, h);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @see #get(String, SimpleHandler.FunctionalHandler) */
|
||||||
|
public HttpServer put(String path, SimpleHandler.FunctionalHandler h) {
|
||||||
|
globalRouter.put(path, h);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @see #get(String, SimpleHandler.FunctionalHandler) */
|
||||||
|
public HttpServer delete(String path, SimpleHandler.FunctionalHandler h) {
|
||||||
|
globalRouter.delete(path, h);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Request processing ---
|
||||||
|
|
||||||
|
private void process(Socket socket) {
|
||||||
|
executorService.submit(() -> {
|
||||||
|
try (socket;
|
||||||
|
InputStream in = socket.getInputStream();
|
||||||
|
OutputStream out = new BufferedOutputStream(socket.getOutputStream())) {
|
||||||
|
while (!stopped) {
|
||||||
|
Request request = RequestParser.parse(in);
|
||||||
|
if (request == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
boolean keepAlive = isKeepAlive(request);
|
||||||
|
|
||||||
|
Response response = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
|
||||||
|
RequestHandler handler = globalRouter.route(request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object result = handler.handle(request, response);
|
||||||
|
if (result instanceof Response r)
|
||||||
|
response = r;
|
||||||
|
else if (result != null)
|
||||||
|
response.setBody(result);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Object result = globalRouter.resolveExceptionHandler(request).handle(ex, request, response);
|
||||||
|
if (result instanceof Response r)
|
||||||
|
response = r;
|
||||||
|
else if (result != null)
|
||||||
|
response.setBody(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain any unread body bytes so the stream is positioned at the next request.
|
||||||
|
request.getBody();
|
||||||
|
|
||||||
|
out.write(HTTP_1_1);
|
||||||
|
writeStatusPhrase(out, response.getStatusCode());
|
||||||
|
out.write(CRLF);
|
||||||
|
out.write(CONTENT_TYPE);
|
||||||
|
out.write(response.getContentType());
|
||||||
|
out.write(CRLF);
|
||||||
|
out.write(CONTENT_LENGTH);
|
||||||
|
writeInt(out, response.getBody() != null ? response.getBody().length : 0);
|
||||||
|
out.write(CRLF);
|
||||||
|
out.write(keepAlive ? CONNECTION_KEEPALIVE : CONNECTION_CLOSE);
|
||||||
|
out.write(CRLF);
|
||||||
|
if (response.getBody() != null)
|
||||||
|
out.write(response.getBody());
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
if (!keepAlive)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (!stopped)
|
||||||
|
log.error("I/O error handling request", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the connection should be kept alive after this request.
|
||||||
|
* HTTP/1.1 default: keep-alive. HTTP/1.0 default: close.
|
||||||
|
* Explicit {@code Connection} header overrides the default.
|
||||||
|
*/
|
||||||
|
private static boolean isKeepAlive(Request request) {
|
||||||
|
String connection = request.getHeader("Connection");
|
||||||
|
if (connection != null) {
|
||||||
|
return !"close".equalsIgnoreCase(connection);
|
||||||
|
}
|
||||||
|
// Detect HTTP version from last byte of protocol field: "HTTP/1.1" → '1',
|
||||||
|
// "HTTP/1.0" → '0'
|
||||||
|
ByteView protocol = request.getRequestLine().getProtocol();
|
||||||
|
return protocol.length() == 8 && protocol.byteAt(7) == '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeStatusPhrase(OutputStream out, int statusCode) throws IOException {
|
||||||
|
byte[] phrase = HttpStatus.bytesForCode(statusCode);
|
||||||
|
if (phrase != null) {
|
||||||
|
out.write(phrase);
|
||||||
|
} else {
|
||||||
|
writeInt(out, statusCode);
|
||||||
|
out.write(UNKNOWN_STATUS_SUFFIX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeInt(OutputStream out, int value) throws IOException {
|
||||||
|
if (value == 0) {
|
||||||
|
out.write(DIGITS[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value < 0) {
|
||||||
|
out.write('-');
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
int divisor = 1;
|
||||||
|
while (value / divisor >= 10)
|
||||||
|
divisor *= 10;
|
||||||
|
while (divisor > 0) {
|
||||||
|
out.write(DIGITS[(value / divisor) % 10]);
|
||||||
|
divisor /= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,8 @@ public class Main {
|
|||||||
throw new RuntimeException(req.getQueryParam("test"));
|
throw new RuntimeException(req.getQueryParam("test"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
server.start().thenRun(() -> log.info("Server started on port: " + PORT));
|
server.start().thenRun(() -> log.info("Server started on port: " + PORT));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-6
@@ -41,9 +41,6 @@ public class RequestParser {
|
|||||||
throw new IOException("Headers too large: buffer exhausted without finding \\r\\n\\r\\n");
|
throw new IOException("Headers too large: buffer exhausted without finding \\r\\n\\r\\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timer starts here — after I/O, measuring only CPU parse time
|
|
||||||
long start = System.nanoTime();
|
|
||||||
|
|
||||||
// 2. Scan Request Line: METHOD PATH PROTOCOL
|
// 2. Scan Request Line: METHOD PATH PROTOCOL
|
||||||
int methodEnd = find(buffer, 0, headerEndIdx, (byte) ' ');
|
int methodEnd = find(buffer, 0, headerEndIdx, (byte) ' ');
|
||||||
if (methodEnd == -1) throw new IOException("Invalid request line (method)");
|
if (methodEnd == -1) throw new IOException("Invalid request line (method)");
|
||||||
@@ -93,9 +90,6 @@ public class RequestParser {
|
|||||||
current = lineEnd + 2; // skip \r\n
|
current = lineEnd + 2; // skip \r\n
|
||||||
}
|
}
|
||||||
|
|
||||||
long elapsed = System.nanoTime() - start;
|
|
||||||
log.debug("Request parsed in {}ns: {} {} {}", elapsed, method, pathView, protocolView);
|
|
||||||
|
|
||||||
// Body is read lazily — only materialized if the handler calls req.getBody().
|
// Body is read lazily — only materialized if the handler calls req.getBody().
|
||||||
// Bytes already in the buffer past \r\n\r\n are handed off to LazyBody as read-ahead.
|
// Bytes already in the buffer past \r\n\r\n are handed off to LazyBody as read-ahead.
|
||||||
int bodyStart = headerEndIdx + 4;
|
int bodyStart = headerEndIdx + 4;
|
||||||
+18
-11
@@ -1,12 +1,10 @@
|
|||||||
package dev.relism.http;
|
package dev.relism.http;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pre-compiled byte representations of standard HTTP status lines (e.g. {@code "200 OK"}).
|
* Pre-compiled byte representations of standard HTTP status lines.
|
||||||
* {@link #bytesForCode(int)} returns the cached array for known codes, {@code null} otherwise.
|
* Uses a direct-access array for O(1) lookup with zero allocation.
|
||||||
*/
|
*/
|
||||||
public enum HttpStatus {
|
public enum HttpStatus {
|
||||||
|
|
||||||
@@ -51,21 +49,30 @@ public enum HttpStatus {
|
|||||||
SERVICE_UNAVAILABLE (503, "Service Unavailable"),
|
SERVICE_UNAVAILABLE (503, "Service Unavailable"),
|
||||||
GATEWAY_TIMEOUT (504, "Gateway Timeout");
|
GATEWAY_TIMEOUT (504, "Gateway Timeout");
|
||||||
|
|
||||||
|
private static final int MAX_STATUS_CODE = 504;
|
||||||
|
private static final byte[][] INDEX = new byte[MAX_STATUS_CODE + 1][];
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (HttpStatus s : values()) {
|
||||||
|
INDEX[s.code] = s.bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final int code;
|
private final int code;
|
||||||
private final byte[] bytes;
|
private final byte[] bytes;
|
||||||
|
|
||||||
private static final Map<Integer, byte[]> INDEX = new HashMap<>();
|
|
||||||
static {
|
|
||||||
for (HttpStatus s : values()) INDEX.put(s.code, s.bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpStatus(int code, String reason) {
|
HttpStatus(int code, String reason) {
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.bytes = (code + " " + reason).getBytes(StandardCharsets.UTF_8);
|
this.bytes = (code + " " + reason).getBytes(StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns pre-compiled {@code "CODE Reason"} bytes for the given code, or {@code null} if unknown. */
|
/** * Returns pre-compiled status bytes for the given code.
|
||||||
|
* Access is O(1) and generates zero garbage.
|
||||||
|
*/
|
||||||
public static byte[] bytesForCode(int code) {
|
public static byte[] bytesForCode(int code) {
|
||||||
return INDEX.get(code);
|
if (code >= 0 && code <= MAX_STATUS_CODE) {
|
||||||
|
return INDEX[code];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
@@ -5,14 +5,21 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>dev.relism</groupId>
|
<groupId>dev.relism</groupId>
|
||||||
<artifactId>Flash</artifactId>
|
<artifactId>flash-parent</artifactId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0-SNAPSHOT</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>flash</module>
|
||||||
|
<module>flash-bench</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<lombok.version>1.18.44</lombok.version>
|
||||||
|
<slf4j.version>2.0.16</slf4j.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
@@ -23,46 +30,46 @@
|
|||||||
</repository>
|
</repository>
|
||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>dev.relism</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>flash</artifactId>
|
||||||
<version>5.11.0</version>
|
<version>${project.version}</version>
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
<version>1.18.44</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.relism</groupId>
|
<groupId>dev.relism</groupId>
|
||||||
<artifactId>fpr-core</artifactId>
|
<artifactId>fpr-core</artifactId>
|
||||||
<version>1.1.0</version>
|
<version>1.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- Logging facade — always on classpath; users provide their own SLF4J binding -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>slf4j-api</artifactId>
|
|
||||||
<version>2.0.16</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- Dev-only binding (simple stdout); excluded from any distributed artifact -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.slf4j</groupId>
|
|
||||||
<artifactId>slf4j-simple</artifactId>
|
|
||||||
<version>2.0.16</version>
|
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.42</version>
|
<version>${lombok.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>${slf4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<version>${slf4j.version}</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.11.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
<pluginManagement>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
@@ -77,12 +84,13 @@
|
|||||||
<path>
|
<path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.44</version>
|
<version>${lombok.version}</version>
|
||||||
</path>
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
package dev.relism;
|
|
||||||
|
|
||||||
import dev.relism.http.ContentType;
|
|
||||||
import dev.relism.http.HttpStatus;
|
|
||||||
import dev.relism.models.*;
|
|
||||||
import dev.relism.routing.AbstractRouter;
|
|
||||||
import dev.relism.routing.GlobalRouter;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entry point for the Flash HTTP server.
|
|
||||||
*
|
|
||||||
* <p>Internally, the server owns a {@link GlobalRouter} with two tiers of routing:
|
|
||||||
* <ol>
|
|
||||||
* <li><b>Mounted sub-routers</b> — registered via {@link #mount}. Each owns a namespace
|
|
||||||
* prefix (e.g. {@code /api}) and handles all paths under it. Matched by longest prefix first.</li>
|
|
||||||
* <li><b>Internal router</b> — the fallback used when no sub-router claims the path.
|
|
||||||
* Routes registered directly on the server ({@link #get}, {@link #post}, etc.) go here.</li>
|
|
||||||
* </ol>
|
|
||||||
*
|
|
||||||
* <p>Error handlers ({@link #onNotFound}, {@link #onException}) registered on the server
|
|
||||||
* apply only to the internal router. Each mounted sub-router has its own independent handlers.
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class HttpServer {
|
|
||||||
private final HttpServerConfiguration configuration;
|
|
||||||
private final ServerSocket serverSocket;
|
|
||||||
private final GlobalRouter globalRouter = new GlobalRouter();
|
|
||||||
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
|
|
||||||
private volatile boolean stopped = false;
|
|
||||||
private final CompletableFuture<Void> readyFuture = new CompletableFuture<>();
|
|
||||||
|
|
||||||
private static final byte[] HTTP_1_1 = "HTTP/1.1 ".getBytes(StandardCharsets.UTF_8);
|
|
||||||
private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8);
|
|
||||||
private static final byte[] CONTENT_TYPE = "Content-Type: ".getBytes(StandardCharsets.UTF_8);
|
|
||||||
private static final byte[] CONTENT_LENGTH = "Content-Length: ".getBytes(StandardCharsets.UTF_8);
|
|
||||||
private static final byte[] CONNECTION_CLOSE = "Connection: close\r\n".getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
private static final byte[] UNKNOWN_STATUS_SUFFIX = " Unknown".getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
private static final byte[][] DIGITS = new byte[10][1];
|
|
||||||
static {
|
|
||||||
for (int i = 0; i < 10; i++) DIGITS[i] = String.valueOf(i).getBytes(StandardCharsets.UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpServer(HttpServerConfiguration configuration) throws IOException {
|
|
||||||
this.configuration = configuration;
|
|
||||||
this.serverSocket = new ServerSocket(configuration.getPort());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the server on a new non-daemon virtual thread and returns a future that completes
|
|
||||||
* when the accept loop is running and the server is ready to serve requests.
|
|
||||||
*/
|
|
||||||
public CompletableFuture<Void> start() {
|
|
||||||
Thread.ofPlatform().name("flash-accept-loop").daemon(false).start(this::run);
|
|
||||||
return readyFuture;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void run() {
|
|
||||||
try (executorService) {
|
|
||||||
readyFuture.complete(null);
|
|
||||||
while (!stopped) {
|
|
||||||
Socket clientSocket = serverSocket.accept();
|
|
||||||
process(clientSocket);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (!stopped) {
|
|
||||||
readyFuture.completeExceptionally(e);
|
|
||||||
log.error("Accept loop error", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the server and waits for in-flight requests to complete.
|
|
||||||
* The returned future is already completed when this method returns.
|
|
||||||
*/
|
|
||||||
public CompletableFuture<Void> stop() {
|
|
||||||
stopped = true;
|
|
||||||
try { serverSocket.close(); } catch (IOException e) { log.error("Error closing server socket", e); }
|
|
||||||
executorService.shutdown();
|
|
||||||
try {
|
|
||||||
if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) executorService.shutdownNow();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
executorService.shutdownNow();
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Routing ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mounts a sub-router under the given namespace prefix.
|
|
||||||
*
|
|
||||||
* <p>Any request whose path starts with {@code namespace} will be dispatched to {@code router}
|
|
||||||
* instead of the internal router. When multiple namespaces match, the longest prefix wins.
|
|
||||||
* The router's own {@code onNotFound} and {@code onException} handlers are used for its paths —
|
|
||||||
* the server-level handlers do not apply.
|
|
||||||
*
|
|
||||||
* @throws dev.relism.exceptions.DuplicateNamespaceException if {@code namespace} is already mounted
|
|
||||||
*/
|
|
||||||
public HttpServer mount(String namespace, AbstractRouter router) {
|
|
||||||
globalRouter.mount(namespace, router);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Global error handlers ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the 404 handler for routes registered directly on this server.
|
|
||||||
* Does not affect mounted sub-routers, which each carry their own not-found handler.
|
|
||||||
*/
|
|
||||||
public HttpServer onNotFound(SimpleHandler.FunctionalHandler handler) {
|
|
||||||
globalRouter.onNotFound(handler);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the exception handler for routes registered directly on this server.
|
|
||||||
* Does not affect mounted sub-routers, which each carry their own exception handler.
|
|
||||||
*/
|
|
||||||
public HttpServer onException(AbstractRouter.ExceptionHandler handler) {
|
|
||||||
globalRouter.onException(handler);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- DSL ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers an annotation-based handler on the internal router.
|
|
||||||
* The handler's class must carry a {@link dev.relism.routing.Route @Route} annotation
|
|
||||||
* declaring the HTTP method and path. To register on a specific sub-router, call
|
|
||||||
* {@link AbstractRouter#register} on that router directly.
|
|
||||||
*/
|
|
||||||
public HttpServer register(RequestHandler handler) { globalRouter.register(handler); return this; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a route on the internal router (not on any mounted sub-router).
|
|
||||||
* The handler's return value is used as the response body; returning a {@link dev.relism.models.Response}
|
|
||||||
* instance replaces the entire response object.
|
|
||||||
*/
|
|
||||||
public HttpServer get(String path, SimpleHandler.FunctionalHandler h) { globalRouter.get(path, h); return this; }
|
|
||||||
/** @see #get(String, SimpleHandler.FunctionalHandler) */
|
|
||||||
public HttpServer post(String path, SimpleHandler.FunctionalHandler h) { globalRouter.post(path, h); return this; }
|
|
||||||
/** @see #get(String, SimpleHandler.FunctionalHandler) */
|
|
||||||
public HttpServer put(String path, SimpleHandler.FunctionalHandler h) { globalRouter.put(path, h); return this; }
|
|
||||||
/** @see #get(String, SimpleHandler.FunctionalHandler) */
|
|
||||||
public HttpServer delete(String path, SimpleHandler.FunctionalHandler h) { globalRouter.delete(path, h); return this; }
|
|
||||||
|
|
||||||
// --- Request processing ---
|
|
||||||
|
|
||||||
private void process(Socket socket) {
|
|
||||||
executorService.submit(() -> {
|
|
||||||
try (socket;
|
|
||||||
InputStream in = socket.getInputStream();
|
|
||||||
OutputStream out = new BufferedOutputStream(socket.getOutputStream())
|
|
||||||
) {
|
|
||||||
long start = System.nanoTime();
|
|
||||||
Request request = RequestParser.parse(in);
|
|
||||||
if (request == null) return;
|
|
||||||
|
|
||||||
Response response = new Response(200, new byte[0], ContentType.TEXT_PLAIN);
|
|
||||||
RequestHandler handler = globalRouter.route(request);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Object result = handler.handle(request, response);
|
|
||||||
if (result instanceof Response r) response = r;
|
|
||||||
else if (result != null) response.setBody(result);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Object result = globalRouter.resolveExceptionHandler(request).handle(ex, request, response);
|
|
||||||
if (result instanceof Response r) response = r;
|
|
||||||
else if (result != null) response.setBody(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
out.write(HTTP_1_1);
|
|
||||||
writeStatusPhrase(out, response.getStatusCode());
|
|
||||||
out.write(CRLF);
|
|
||||||
out.write(CONTENT_TYPE);
|
|
||||||
out.write(response.getContentType());
|
|
||||||
out.write(CRLF);
|
|
||||||
out.write(CONTENT_LENGTH);
|
|
||||||
writeInt(out, response.getBody() != null ? response.getBody().length : 0);
|
|
||||||
out.write(CRLF);
|
|
||||||
out.write(CONNECTION_CLOSE);
|
|
||||||
out.write(CRLF);
|
|
||||||
if (response.getBody() != null) out.write(response.getBody());
|
|
||||||
out.flush();
|
|
||||||
log.info("{} ms: {}", (System.nanoTime() - start) / 1_000_000.0, request.getRequestLine());
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("I/O error handling request", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeStatusPhrase(OutputStream out, int statusCode) throws IOException {
|
|
||||||
byte[] phrase = HttpStatus.bytesForCode(statusCode);
|
|
||||||
if (phrase != null) {
|
|
||||||
out.write(phrase);
|
|
||||||
} else {
|
|
||||||
writeInt(out, statusCode);
|
|
||||||
out.write(UNKNOWN_STATUS_SUFFIX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static void writeInt(OutputStream out, int value) throws IOException {
|
|
||||||
if (value == 0) { out.write(DIGITS[0]); return; }
|
|
||||||
if (value < 0) { out.write('-'); value = -value; }
|
|
||||||
int divisor = 1;
|
|
||||||
while (value / divisor >= 10) divisor *= 10;
|
|
||||||
while (divisor > 0) { out.write(DIGITS[(value / divisor) % 10]); divisor /= 10; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user