Blade 简介
1. 概述
Blade 是一个小型的 Java 8+ MVC 框架,从头开始构建,并牢记一些明确的目标:独立、高效、优雅、直观和超快。
许多不同的框架启发了它的设计:Node 的Express 、Python 的Flask 和 Golang 的Macaron / Martini 。
Blade 也是一个雄心勃勃的大型项目Let’s Blade 的 一部分。它包括其他小型库的异构集合,从验证码生成到 JSON 转换,从模板到简单的数据库连接。
但是,在本教程中,我们将只关注 MVC。
2. 入门
首先,让我们创建一个空的 Maven 项目,并在pom.xml中添加最新的 Blade MVC 依赖 项:
<dependency>
<groupId>com.bladejava</groupId>
<artifactId>blade-mvc</artifactId>
<version>2.0.14.RELEASE</version>
</dependency>
2.1. 创建应用程序
由于我们的应用程序将被创建为 JAR,因此它不会像在 WAR 中那样具有*/lib文件夹。因此,这使我们遇到了如何将刀片-mvc* JAR 以及我们可能需要的任何其他 JAR 提供给我们的应用程序的问题。
如何使用 Maven 创建可执行 JAR 教程中解释了执行此操作的不同方法,每种方法各有利弊。
为简单起见,我们将使用Maven 组装插件技术,该技术会分解 pom.xml中导入的任何 JAR ,然后将所有类捆绑在一个 uber-JAR 中。
2.2. 运行应用程序
Blade 基于Netty ,一个惊人的异步事件驱动网络应用程序框架。因此,要运行我们基于 Blade 的应用程序,我们不需要任何外部应用程序服务器或 Servlet 容器;JRE就足够了:
java -jar target/sample-blade-app.jar
之后,可以通过http://localhost:9000 URL 访问该应用程序。
3. 了解架构
Blade 的架构非常简单:
它始终遵循相同的生命周期:
- Netty 收到请求
- 执行中间件(可选)
- 执行 WebHooks(可选)
- 执行路由
- 响应被发送到客户端
- 清理
我们将在接下来的部分中探讨上述功能。
4. 路由
简而言之,MVC 中的路由是用于在 URL 和 Controller 之间创建绑定的机制。
Blade 提供了两种类型的路由:基本路由和带注释路由。
4.1. 基本路由
基本路由适用于非常小的软件,例如微服务或最小的 Web 应用程序:
Blade.of()
.get("/basic-routes-example", ctx -> ctx.text("GET called"))
.post("/basic-routes-example", ctx -> ctx.text("POST called"))
.put("/basic-routes-example", ctx -> ctx.text("PUT called"))
.delete("/basic-routes-example", ctx -> ctx.text("DELETE called"))
.start(App.class, args);
用于注册路由的方法的名称对应于将用于转发请求的 HTTP 动词。就如此容易。
在这种情况下,我们返回一个文本,但我们也可以渲染页面,我们将在本教程后面看到。
4.2. 带注释的路线
当然,对于更实际的用例,我们可以使用注释定义我们需要的所有路由。我们应该为此使用单独的类。
首先,我们需要通过*@Path*注解创建一个Controller,在启动的时候会被Blade扫描。
然后我们需要使用与我们要拦截的 HTTP 方法相关的路由注解:
@Path
public class RouteExampleController {
@GetRoute("/routes-example")
public String get(){
return "get.html";
}
@PostRoute("/routes-example")
public String post(){
return "post.html";
}
@PutRoute("/routes-example")
public String put(){
return "put.html";
}
@DeleteRoute("/routes-example")
public String delete(){
return "delete.html";
}
}
我们还可以使用简单的*@Route*注解并将 HTTP 方法指定为参数:
@Route(value="/another-route-example", method=HttpMethod.GET)
public String anotherGet(){
return "get.html" ;
}
另一方面,如果我们不放置任何方法参数,则路由将拦截对该 URL 的每个 HTTP 调用,无论动词如何。
4.3. 参数注入
有几种方法可以将参数传递给我们的路由。让我们通过文档 中的一些示例来探索它们。
- 表单参数:
@GetRoute("/home")
public void formParam(@Param String name){
System.out.println("name: " + name);
}
- 宁静参数:
@GetRoute("/users/:uid")
public void restfulParam(@PathParam Integer uid){
System.out.println("uid: " + uid);
}
- 文件上传参数:
@PostRoute("/upload")
public void fileParam(@MultipartParam FileItem fileItem){
byte[] file = fileItem.getData();
}
- 头参数:
@GetRoute("/header")
public void headerParam(@HeaderParam String referer){
System.out.println("Referer: " + referer);
}
- cookie参数:
@GetRoute("/cookie")
public void cookieParam(@CookieParam String myCookie){
System.out.println("myCookie: " + myCookie);
}
- body参数:
@PostRoute("/bodyParam")
public void bodyParam(@BodyParam User user){
System.out.println("user: " + user.toString());
}
- 值对象参数,通过将其属性发送到路由来调用:
@PostRoute("/voParam")
public void voParam(@Param User user){
System.out.println("user: " + user.toString());
}
<form method="post">
<input type="text" name="age"/>
<input type="text" name="name"/>
</form>
5. 静态资源
如果需要,Blade 还可以提供静态资源,只需将它们放在*/resources/static*文件夹中即可。
例如,src/main/resources/static/app.css将在 http://localhost:9000/static/app.css 可用。
5.1.自定义路径
我们可以通过以编程方式添加一个或多个静态路径来调整此行为:
blade.addStatics("/custom-static");
通过编辑文件src/main/resources/application.properties可以通过配置获得相同的结果:
mvc.statics=/custom-static
5.2. 启用资源列表
我们可以允许列出静态文件夹的内容,出于安全原因,该功能默认关闭:
blade.showFileList(true);
或者在配置中:
mvc.statics.show-list=true
我们现在可以打开 http://localhost:9000/custom-static/ 来显示文件夹的内容。
5.3. 使用 WebJars
正如WebJars 简介 教程中所见,打包为 JAR 的静态资源也是一个可行的选择。
Blade 在*/webjars/*路径下自动公开它们。
例如,让我们在pom.xml中导入Bootstrap :
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.2.1</version>
</dependency>
因此,它将在http://localhost:9000/webjars/bootstrap/4.2.1/css/bootstrap.css下可用
6. HTTP 请求
由于Blade 不是基于 Servlet 规范,其接口*Request 及其类HttpRequest *等对象与我们习惯的对象略有不同。
6.1. 表单参数
在读取表单参数时,Blade 在查询方法的结果中充分利用了 Java 的Optional(以下所有方法都返回一个 Optional 对象):
- query(String name)
- queryInt(String name)
- queryLong(String name)
- queryDouble(String name)
它们还具有备用值:
- String query(String name, String defaultValue)
- int queryInt(String name, int defaultValue)
- long queryLong(String name, long defaultValue)
- double queryDouble(String name, double defaultValue)
我们可以通过 automapped 属性读取表单参数:
@PostRoute("/save")
public void formParams(@Param String username){
// ...
}
或者来自Request对象:
@PostRoute("/save")
public void formParams(Request request){
String username = request.query("username", "Blogdemo");
}
6.2. JSON数据
现在让我们看看如何将 JSON 对象映射到 POJO:
curl -X POST http://localhost:9000/users -H 'Content-Type: application/json' \
-d '{"name":"Blogdemo","site":"blogdemo.com"}'
POJO(用Lombok 注释以提高可读性):
public class User {
@Getter @Setter private String name;
@Getter @Setter private String site;
}
同样,该值可用作注入的属性:
@PostRoute("/users")
public void bodyParams(@BodyParam User user){
// ...
}
并从Request:
@PostRoute("/users")
public void bodyParams(Request request) {
String bodyString = request.bodyToString();
}
6.3. RESTful 参数
像localhost:9000/user/42 这样漂亮的 URL 中的 RESTFul 参数也是一等公民:
@GetRoute("/user/:id")
public void user(@PathParam Integer id){
// ...
}
像往常一样,我们可以在需要时依赖Request对象:
@GetRoute("/user")
public void user(Request request){
Integer id = request.pathInt("id");
}
显然,Long和String类型也可以使用相同的方法。
6.4. 数据绑定
Blade 支持 JSON 和 Form 绑定参数,并自动将它们附加到模型对象:
@PostRoute("/users")
public void bodyParams(User user){}
6.5. 请求和会话属性
在Request和 Session中读写对象的 API 非常清晰。
具有两个参数(表示键和值)的方法是我们可以用来将值存储在不同上下文中的修改器:
Session session = request.session();
request.attribute("request-val", "Some Request value");
session.attribute("session-val", 1337);
另一方面,仅接受 key 参数的相同方法是访问器:
String requestVal = request.attribute("request-val");
String sessionVal = session.attribute("session-val"); //It's an Integer
一个有趣的特性是它们的通用返回类型 T ,它使我们无需转换结果。
6.6. 标头
相反,请求标头只能从请求中读取:
String header1 = request.header("a-header");
String header2 = request.header("a-safe-header", "with a default value");
Map<String, String> allHeaders = request.headers();
6.7. 实用程序
以下实用方法也可以开箱即用,它们非常明显,不需要进一步解释:
- boolean isIE()
- boolean isAjax()
- String contentType()
- String userAgent()
6.8. 读Cookie
让我们看看Request对象如何帮助我们处理 Cookie,特别是在读 Cookie时:
Optional<Cookie> cookieRaw(String name);
如果 Cookie 不存在,我们还可以通过指定要应用的默认值将其作为 *String *获取:
String cookie(String name, String defaultValue);
最后,这是我们一次读取所有 Cookie 的方式(*keys *是 Cookie 的名称,*values *是 Cookie 的值):
Map<String, String> cookies = request.cookies();
7. HTTP 响应
类似于对Request所做的事情,我们可以通过简单地将其声明为路由方法的参数来获得对Response对象的引用:
@GetRoute("/")
public void home(Response response) {}
7.1.简单输出
我们可以通过一种方便的输出方法轻松地向调用者发送简单的输出,以及 200 HTTP 代码和适当的 Content-Type。
首先,我们可以发送一个纯文本:
response.text("Hello World!");
其次,我们可以生成一个 HTML:
response.html("<h1>Hello World!</h1>");
第三,我们同样可以生成一个 XML:
response.xml("<Msg>Hello World!</Msg>");
最后,我们可以使用String输出 JSON :
response.json("{\"The Answer\":42}");
甚至来自 POJO,利用自动 JSON 转换:
User user = new User("Blogdemo", "blogdemo.com");
response.json(user);
7.2. 文件输出
从服务器下载文件再简单不过了:
response.download("the-file.txt", "/path/to/the/file.txt");
第一个参数设置将要下载的文件的名称,而第二个参数(一个File对象,这里用String构造)表示服务器上实际文件的路径。
7.3. 模板渲染
Blade 还可以通过模板引擎渲染页面:
response.render("admin/users.html");
模板默认目录是src/main/resources/templates/,因此前面的单行将查找文件 src/main/resources/templates/admin/users.html。
稍后我们将在模板部分了解更多信息。
7.4. 重定向
重定向意味着向浏览器发送一个 302 HTTP 代码,以及一个 URL,后面跟着第二个 GET。
我们可以重定向到另一个路由,或者也可以重定向到一个外部 URL:
response.redirect("/target-route");
7.5.写Cookie
在这一点上,我们应该习惯 Blade 的简单性。因此,让我们看看如何在一行代码中编写一个未过期的 Cookie:
response.cookie("cookie-name", "Some value here");
事实上,删除 Cookie 也同样简单:
response.removeCookie("cookie-name");
7.6. 其他操作
最后,Response对象为我们提供了其他几种方法来执行诸如编写 Headers、设置 Content-Type、设置状态码等操作。
让我们快速浏览一下其中的一些:
- Response status(int status)
- Map headers()
- Response notFound()
- Map cookies()
- Response contentType(String contentType)
- void body(@NonNull byte[] data)
- Response header(String name, String value)
8. WebHooks
WebHook 是一个拦截器,通过它我们可以在执行路由方法之前和之后运行代码。
我们可以通过简单地实现WebHook功能接口并覆盖*before()*方法来创建 WebHook:
@FunctionalInterface
public interface WebHook {
boolean before(RouteContext ctx);
default boolean after(RouteContext ctx) {
return true;
}
}
正如我们所见,*after()*是一个默认方法,因此我们只会在需要时覆盖它。
8.1. 拦截每个请求
@Bean注解告诉框架使用 IoC 容器扫描类。
因此,使用它注释的 WebHook 将在全局范围内工作,拦截对每个 URL 的请求:
@Bean
public class BlogdemoHook implements WebHook {
@Override
public boolean before(RouteContext ctx) {
System.out.println("[BlogdemoHook] called before Route method");
return true;
}
}
8.2. 缩小到 URL
我们还可以拦截特定的 URL,仅围绕这些路由方法执行代码:
Blade.of()
.before("/user/*", ctx -> System.out.println("Before: " + ctx.uri()));
.start(App.class, args);
8.3. 中间件
中间件是优先的 WebHook,在任何标准 WebHook 之前执行:
public class BlogdemoMiddleware implements WebHook {
@Override
public boolean before(RouteContext context) {
System.out.println("[BlogdemoMiddleware] called before Route method and other WebHooks");
return true;
}
}
它们只需要在没有*@Bean注释的情况下定义,然后通过use()* 以声明方式注册:
Blade.of()
.use(new BlogdemoMiddleware())
.start(App.class, args);
此外,Blade 还附带以下与安全相关的内置中间件,其名称应一目了然:
9. 配置
**在 Blade 中,配置是完全可选的,因为按照惯例,一切都是开箱即用的。**但是,我们可以自定义默认设置,并在src/main/resources/application.properties文件中引入新属性。
9.1. 读取配置
我们可以通过不同的方式读取配置,可以指定或不指定默认值,以防设置不可用。
- 启动期间:
Blade.of()
.on(EventType.SERVER_STARTED, e -> {
Optional<String> version = WebContext.blade().env("app.version");
})
.start(App.class, args);
- 路由内部:
@GetRoute("/some-route")
public void someRoute(){
String authors = WebContext.blade().env("app.authors","Unknown authors");
}
- 在自定义加载器中,通过实现BladeLoader接口,覆盖load()方法,并使用@Bean注释类:
@Bean
public class LoadConfig implements BladeLoader {
@Override
public void load(Blade blade) {
Optional<String> version = WebContext.blade().env("app.version");
String authors = WebContext.blade().env("app.authors","Unknown authors");
}
}
9.2. 配置属性
已配置但准备自定义的几个设置按类型分组,并在此地址 的三列表(名称、描述、默认值)中列出。我们也可以参考翻译页面,注意翻译错误地将设置名称大写。实际设置完全小写。
按前缀对配置设置进行分组可以将它们一次全部读取到地图中,这在它们很多时很有用:
Environment environment = blade.environment();
Map<String, Object> map = environment.getPrefix("app");
String version = map.get("version").toString();
String authors = map.get("authors","Unknown authors").toString();
9.3. 处理多个环境
将应用程序部署到不同的环境时,我们可能需要指定不同的设置,例如与数据库连接相关的设置。 Blade 为我们提供了一种为不同环境配置应用程序的方法,而不是手动替换 application.properties 文件。 我们可以简单地保留 application.properties 与所有开发设置,然后在同一文件夹中创建其他文件,例如 application-prod.properties,仅包含不同的设置。
在启动过程中,我们可以指定要使用的环境,框架将使用 application-prod.properties 中最具体的设置以及默认 application.properties 文件中的所有其他设置来合并文件:
java -jar target/sample-blade-app.jar --app.env=prod
10. 模板化
Blade 中的模板化是一个模块化的方面。 虽然它集成了一个非常基本的模板引擎,但对于视图的任何专业用途,我们应该依赖外部模板引擎。 然后,我们可以从 GitHub 上的 Blade-template-engines 存储库中提供的引擎中选择一个引擎,其中包括 FreeMarker、Jetbrick、Pebble 和 Velocity,甚至可以创建一个包装器来导入我们喜欢的另一个模板。
Blade 的作者推荐了另一个中文项目 Jetbrick 。
10.1. 使用默认引擎
默认模板的工作原理是通过* ${} *表示法解析来自不同上下文的变量:
<h1>Hello, ${name}!</h1>
10.2. 插入外部引擎
切换到不同的模板引擎是轻而易举的事! 我们只需导入引擎(的 Blade 包装器)的依赖项:
<dependency>
<groupId>com.bladejava</groupId>
<artifactId>blade-template-jetbrick</artifactId>
<version>0.1.3</version>
</dependency>
此时,编写一个简单的配置来指示框架使用该库就足够了:
@Bean
public class TemplateConfig implements BladeLoader {
@Override
public void load(Blade blade) {
blade.templateEngine(new JetbrickTemplateEngine());
}
}
因此,现在*src/main/resources/templates/*下的每个文件都将使用新引擎进行解析,其语法超出了本教程的范围。
10.3. 包装新引擎
包装新的模板引擎需要创建一个类,该类必须实现TemplateEngine接口并重写*render()*方法:
void render (ModelAndView modelAndView, Writer writer) throws TemplateException;
为此,我们可以查看实际 Jetbrick 包装器的代码 以了解其含义。
11. 日志记录
Blade 使用slf4j-api作为日志记录接口。
它还包括一个已配置的日志记录实现,称为blade-log 。因此,我们不需要导入任何东西;它按原样工作,只需定义一个Logger:
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
11.1. 自定义日志
如果我们想修改默认配置,我们需要调整以下参数作为系统属性:
- 日志记录级别(可以是“trace”, “debug”, “info”, “warn”, or “error”):
# Root Logger
com.blade.logger.rootLevel=info
# Package Custom Logging Level
com.blade.logger.somepackage=debug
# Class Custom Logging Level
com.blade.logger.com.blogdemo.sample.SomeClass=trace
- 显示信息:
# Date and Time
com.blade.logger.showDate=false
# Date and Time Pattern
com.blade.logger.datePattern=yyyy-MM-dd HH:mm:ss:SSS Z
# Thread Name
com.blade.logger.showThread=true
# Logger Instance Name
com.blade.logger.showLogName=true
# Only the Last Part of FQCN
com.blade.logger.shortName=true
- Logger:
# Path
com.blade.logger.dir=./logs
# Name (it defaults to the current app.name)
com.blade.logger.name=sample
11.2. 排除日志记录器
尽管已经配置了日志记录器对于启动我们的小项目非常方便,但我们可能很容易遇到其他库导入自己的日志记录实现的情况。而且,在这种情况下,我们可以删除集成的以避免冲突:
<dependency>
<groupId>com.bladejava</groupId>
<artifactId>blade-mvc</artifactId>
<version>${blade.version}</version>
<exclusions>
<exclusion>
<groupId>com.bladejava</groupId>
<artifactId>blade-log</artifactId>
</exclusion>
</exclusions>
</dependency>
12. 定制
12.1. 自定义异常处理
框架中还默认内置了异常处理程序。它将异常打印到控制台,如果 app.devMode 为 true,则堆栈跟踪也可以在网页上看到。
但是,我们可以通过定义扩展 DefaultExceptionHandler 类的 @Bean 以特定方式处理异常:
@Bean
public class GlobalExceptionHandler extends DefaultExceptionHandler {
@Override
public void handle(Exception e) {
if (e instanceof BlogdemoException) {
BlogdemoException blogdemoException = (BlogdemoException) e;
String msg = blogdemoException.getMessage();
WebContext.response().json(RestResponse.fail(msg));
} else {
super.handle(e);
}
}
}
12.2. 自定义错误页面
同样,错误 404 – 未找到 和 500 – 内部服务器错误 是通过瘦默认页面处理的。
我们可以通过在application.properties文件中使用以下设置声明它们来强制框架使用我们自己的页面:
mvc.view.404=my-404.html
mvc.view.500=my-500.html
当然,这些HTML页面必须放在src/main/resources/templates文件夹下。
在 500 中,我们还可以通过特殊变量检索异常 message 和 stackTrace :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>500 Internal Server Error</title>
</head>
<body>
<h1> Custom Error 500 Page </h1>
<p> The following error occurred: "<strong>${message}</strong>"</p>
<pre> ${stackTrace} </pre>
</body>
</html>
13. 计划任务
该框架的另一个有趣的功能是可以调度方法的执行。
这可以通过使用 @Schedule 注释来注释 @Bean 类的方法来实现:
@Bean
public class ScheduleExample {
@Schedule(name = "blogdemoTask", cron = "0 */1 * * * ?")
public void runScheduledTask() {
System.out.println("This is a scheduled Task running once per minute.");
}
}
事实上,它使用经典的 cron 表达式来指定 DateTime 坐标。我们可以在Cron 表达式指南 中阅读更多相关内容。
稍后,我们可能会利用TaskManager 类的静态方法对计划任务执行操作。
- 获取所有计划任务:
List<Task> allScheduledTasks = TaskManager.getTasks();
- 按名称获取任务:
Task myTask = TaskManager.getTask("blogdemoTask");
- 按名称停止任务:
boolean closed = TaskManager.stopTask("blogdemoTask");
14. 事件
正如第 9.1 节中所见,可以在运行某些自定义代码之前监听指定的事件。
Blade 提供以下开箱即用的事件:
public enum EventType {
SERVER_STARTING,
SERVER_STARTED,
SERVER_STOPPING,
SERVER_STOPPED,
SESSION_CREATED,
SESSION_DESTROY,
SOURCE_CHANGED,
ENVIRONMENT_CHANGED
}
虽然前六个很容易猜到,但后两个需要一些提示: ENVIRONMENT_CHANGED 允许我们在服务器启动时配置文件发生更改时执行操作。相反, SOURCE_CHANGED 尚未实现,仅供将来使用。
让我们看看如何在创建会话时在会话中添加一个值:
Blade.of()
.on(EventType.SESSION_CREATED, e -> {
Session session = (Session) e.attribute("session");
session.attribute("name", "Blogdemo");
})
.start(App.class, args);
15. 会话实现
谈到会话,它的默认实现将会话值存储在内存中。
因此,我们可能希望切换到不同的实现来提供缓存、持久性或其他功能。我们以 Redis 为例。我们首先需要通过实现Session 接口来创建RedisSession包装器,如HttpSession的文档 中所示。
然后,只需让框架知道我们想要使用它即可。我们可以按照与自定义模板引擎相同的方式执行此操作,唯一的区别是我们调用 sessionType() 方法:
@Bean
public class SessionConfig implements BladeLoader {
@Override
public void load(Blade blade) {
blade.sessionType(new RedisSession());
}
}
16. 命令行参数
从命令行运行 Blade 时,我们可以指定三个设置来改变其行为。
首先,我们可以更改IP地址,默认是本地 0.0.0.0 回:
java -jar target/sample-blade-app.jar --server.address=192.168.1.100
其次,我们还可以更改端口,默认为 9000:
java -jar target/sample-blade-app.jar --server.port=8080
最后,如第 9.3 节所示,我们可以更改环境,让不同的application-XXX.properties文件在默认文件application.properties上被读取:
java -jar target/sample-blade-app.jar --app.env=prod
17. 在IDE中运行
任何现代 Java IDE 都可以运行 Blade 项目,甚至不需要 Maven 插件。在运行Blade 演示 (专门为展示框架功能而编写的示例)时,在 IDE 中运行 Blade 特别有用。它们都继承了父 pom,因此更容易让 IDE 完成工作,而不是手动调整它们以作为独立应用程序运行。
17.1. Eclipse
在 Eclipse 中,右键单击该项目并启动 Run as Java Application ,选择我们的 App 类,然后按 OK 就足够了。
然而,Eclipse 的控制台不会正确显示 ANSI 颜色,而是倒出它们的代码:
幸运的是,在控制台扩展中安装ANSI Escape 可以很好地解决这个问题:
17.2. IntelliJ IDEA
IntelliJ IDEA 可以直接使用 ANSI 颜色。因此,创建项目就足够了,右键单击 App 文件,然后启动 Run ‘App.main()’(相当于按Ctrl+Shift+F10
):
17.3. Visual Studio Code
通过预先安装Java 扩展包 ,还可以使用 VSCode(一种流行的非以 Java 为中心的 IDE)。
按Ctrl+F5
将运行该项目: