跳到主要内容
JackSparrow414
返回

Eclipse Krazo(Jakarta MVC)的使用

本文导航

展开本文导航

背景

在N年以前,我估计在零几年?对我等95后开发者来说,可能意味着上古时代?提起MVC那时候还是Struts的时代,后来随着Spring的出现,SpringMVC开始渐渐被越来越多的开发者使用。Struts走向没落,中间还出现Vraptor,后来Struts2发布,Struts的生命彻底走到尽头。因为Spring不是Jakarta EE的规范,所以对于Jakarta EE来说一直缺少MVC规范,终于在近几年,社区开始建立MVC规范。

目前MVC框架大概是三家,SpringMVC, Struts2, Eclipse Krazo(Jakarta MVC规范的实现)。

Jakarta MVC规范

最新的规范是2.1.规范内容非常少,不复杂。建议阅读一遍规范,基本就知道怎么用了。Jakarta MVC是建立在Jakarta Restful Web Services(jakarta.ws.rs)之上的,并支持CDI

来自官方规范1.4节 Most of the terminology used in this specification is borrowed from other specifications such as Jakarta RESTful Web Services and Jakarta Contexts and Dependency Injection

关于Jakarta Restful Web Services的实现RestEasy已经在RestEasy的入门与使用这篇文章里做过详细的介绍,这里不做过多展开。

Eclipse Krazo

krazo官方网站,文档入口如下图 Eclipse Krazo 下载页中的文档入口 文档内容同样不多,估计几分钟就看完了。在一开始的时候,可以和上面的规范文档一起看。

使用前的思考

有使用过MVC框架的同学对MVC整体的开发流程是很清楚的,基本一个典型的业务流程是如下:

  1. 访问A页面,A.jsp/A.xhtml(jsf)。请求相应的Controller里的方法accessPageA(),将A页面用到的model设置进去,然后在A页面使用EL表达式进行取值。
  2. 进入A页面,进行一些操作,提交表单。请求相应的Controller里的submit()方法,对参数进行校验,处理业务,根据不同的结果跳转到不同的页面。

下面的例子就以这两步为例,给出一些简单的示例代码

全局配置

@ApplicationPath("mvc")
public class MvcApplication extends Application {

    /**
     * 配置文件 https://eclipse-ee4j.github.io/krazo/documentation/latest/index.html#_properties_default_view_file_extension_org_eclipse_krazo_defaultviewfileextension
     * @return
     */
    @Override
    public Map<String, Object> getProperties() {
        final Map<String, Object> properties = new HashMap<>();
        // 设置页面(View)文件夹
        properties.put(ViewEngine.DEFAULT_VIEW_FOLDER, "/WEB-INF/views/");
        properties.put(Properties.DEFAULT_VIEW_FILE_EXTENSION, "jsp");
        // 设置form表单的method属性,允许除了get和post之外的其他请求
        properties.put(FormMethodOverwriter.FORM_METHOD_OVERWRITE, Options.ENABLED);
        properties.put(FormMethodOverwriter.HIDDEN_FIELD_NAME, FormMethodOverwriter.DEFAULT_HIDDEN_FIELD_NAME);
        return properties;
    }
}

Controller示例

主要注解就是@Controller, 注意这里我的注解是放在class上的,代表该类下所有的方法都是返回到页面(View)。如果你只需要某个方法返回页面,其他方法是Restful的,那么只需要把Controller注解放到相应的方法上即可。

@RequestScoped
@Controller
@Path("test")
public class MvcController {

    @Inject
    private Models models;

    /**
     * 获得校验结果
     */
    @Inject
    private BindingResult bindingResult;

    @GET
    @Path("helloMvc/{path}")
    public Response helloMvc(@PathParam("path") String path) {
        models.put("message", "Hello MVC, " + path);
        return Response.ok("helloMvc.jsp").build();
    }

    @DELETE
    @Path("deleteMvc")
    public String deleteMvc(@FormParam("message") @MvcBinding @NotBlank String message) {
        if (bindingResult.isFailed()) {
            models.put("errors", bindingResult.getAllMessages());
            return "deleteMvcResult.jsp";
        }
        models.put("message", message);
        return "deleteMvcResult.jsp";
    }

    @POST
    @Path("csrf")
    @CsrfProtected
    @View("csrf.jsp")
    public void csrf() {
        models.put("message", "csrf");
    }
}

返回View的三种写法

如上述代码所示

  1. 通过Response对象设置页面路径
  2. 返回String,String内容是页面路径
  3. 使用@View注解,注解内容是页面路径

View中用到的Model如何设值?

This specification supports two kinds of models: the first is based on CDI @Named beans, and the second on the Models interface which defines a map between names and objects. Jakarta MVC provides view engines for Jakarta Server Pages and Facelets out of the box, which support both types.

官方文档提供了Model设值两种方式,这里示例代码使用Models这个接口,直接通过CDI注入进来。

View中如何获取Model中的值?

以JSP为例,可以使用EL表达式或者request.getAttribute()方法

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--jstl 3.0的uri已经改变了--%>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<html>
<head>
    <title>helloMvc</title>
</head>
<body>
<div>
    这是返回给页面的信息:${message}<br>
    这是使用jstl标签库的信息:<c:out value="${message}"/><br>
<%--    https://jakarta.ee/specifications/mvc/2.1/jakarta-mvc-spec-2.1.html#view_engines--%>
    这是使用request.getAttribute()方法获取的信息:<%=request.getAttribute("message")%><br>
</div>
<div>
    <form action="${pageContext.request.contextPath}/mvc/test/deleteMvc" method="POST">
        <input type="hidden" name="_method" value="DELETE"/>
        <input type="text" name="message" value="${message}"/>
        <input type="submit" value="提交"/>
    </form>
</div>
<div>
    <form action="${pageContext.request.contextPath}/mvc/test/csrf" method="post">
<%--       https://jakarta.ee/specifications/mvc/2.1/jakarta-mvc-spec-2.1.html#mvc_context --%>
        <input type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}"/>
        <input type="submit" value="测试csrf"/>
    </form>
</div>
</body>
</html>

为什么request.getAttribute能够获取到model的值呢?这是因为规范要求,实现此规范时,需要将model通过setAttribute的方式设置进去 Jakarta MVC 规范要求将 model 绑定到 Servlet 请求属性 在Krazo源码ServletViewEngine中也可以看到这一步 ServletViewEngine 遍历 model 并调用 request.setAttribute

重定向

可以在一个方法中直接重定向到另外一个页面

@GET
    @Path("buildUrl")
    public Response buildUrl() {
        Map<String, Object> map = Map.of("path", "buildUrl");
        String path = mvcContext.uri("MvcController#helloMvc", map).getPath();
//        path is /mvc-demo/mvc/test/helloMvc/buildUrl; 由于请求中带有mvc-demo/mvc,所以需要去掉
        URI uri = UriBuilder.fromPath("../.."+path).buildFromMap(map);
        return Response.seeOther(uri).build();
    }

写法: 要重定向的类名#方法名

The controller method is referenced using the simple name of the controller class and the corresponding method name separated by #. If the URI contains path, query or matrix parameters, concrete values can be supplied using a map. Please note that the keys of this map must match the parameter name used in the @PathParam, @QueryParam or @MatrixParam annotation

上述写法如果在JSP中,也可以使用EL表达式${mvc.uri(‘className#methodName’)}

参数校验

使用@MvcBinding注解.使用方法如上述代码所示,关于各种校验注解可以直接使用Jakarta Bean Validation 相关注解。如果想要了解更多可以参考Hibernate-Validator使用这篇博文。

当校验失败时,我们这里的处理方式是,把校验失败的信息放到errors中,并返回一个页面,在页面中展示。

防止CSRF

关于CSRF的更多内容请移步OWASP Cheat Sheet CSRF一节。

要集成防止CSRF攻击,官方也提供了具体的规范要求和实现。如上述代码所示,只要在表单里添加一个隐藏域,隐藏域的name和value是EL表达式

<input type="hidden" name="${mvc.csrf.name}" value="${mvc.csrf.token}" />

在进入JSP页面时,通过EL表达式获得token,和在后端方法上加上@CsrfProtected注解即可

Krazo是如何实现的呢?

如何生成csrf的token?

上述代码中的

${mvc.csrf.name}和${mvc.csrf.token}

这两个EL表达式完成了name和value的生成.关键源码如下 入口点是CsrfProtectFilter类 CsrfProtectFilter 生成 CSRF token 并保存到 session 的源码 最终放到session里。

这里你可能会疑惑,为什么这里的EL表达式是mvc呢,那是因为 规范规定 Jakarta MVC 规范说明 MvcContext 可通过 mvc 名称注入视图 规范实现 Krazo MvcContextImpl 使用 @Named("mvc") 暴露上下文

如何校验csrf呢?

入口点是CsrfValidateFilter CsrfValidateFilter 根据 CSRF 配置检查并验证请求的源码 CSRF 校验从 session 和请求取出 token 并比较的代码 很简单,就是去session里拿一下,验证一下。如果校验不通过则抛出异常

自定义失败页面

虽然官方提供了一个默认的实现CsrfExceptionMapper,但是我们想CSRF校验失败时,自己控制跳到自己项目里的页面

@Provider
@Priority(Priorities.USER)
public class MvcExceptionHandler implements ExceptionMapper<CsrfValidationException> {

    @Context
    private HttpServletResponse response;

    @Context
    public HttpServletRequest request;

    @Context
    private ServletContext servletContext;

    @SneakyThrows
    @Override
    public Response toResponse(final CsrfValidationException e) {
        request.setAttribute("errors", e.getMessage());
//        两种写法都可以
//        servletContext.getRequestDispatcher("/WEB-INF/views/csrf.jsp").forward(request, response);
        request.getServletContext().getRequestDispatcher("/WEB-INF/views/csrf.jsp").forward(request, response);
        return null;
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<html>
<head>
    <title>csrf</title>
</head>
<body>
<c:if test="${not empty errors}">
    csrf校验失败:<br>
    <ul>
        <c:forEach items="${errors}" var="error">
            <li>${error}</li>
        </c:forEach>
    </ul>
    <br>
</c:if>
<c:if test="${not empty message}">
    ${message} 校验成功
</c:if>
</body>
</html>

其他用法以及配置

关于本篇博文中没有提到的其他配置以及用法,可以参考Krazo官方文档 Configuration一节

备注


分享本文:

上一篇
编程可视化网站与软件(长期更新)
下一篇
C语言VS Code 开发环境搭建

评论

欢迎提问、纠错和分享经验。发表评论需要登录 GitHub;中英文版本共享评论。

评论仅在正式网站开放。