从此

Java Web应用技术 - Servlet(web.xml)、JAX-RS(SSE)、JSF | JavaWeb、Jakarta EE(Jakarta Data) | Open Liberty


综合

Servlet web.xml写法

Spring框架

SpringBoot内嵌Web Server容器 - Tomcat,Jetty,Undertow,Netty

Nginx集成Java模块 - nginx-clojure

JSF(Jakarta Server Faces/Java Server Faces) - https://jakarta.ee/specifications/faces/


Jakarta EE Servlet API

Servlet+Gradle实例:

  Java Servlet 技术入门

plugins { id "java" id "org.gretty" version "4.1.1" } repositories { mavenCentral() } dependencies { compileOnly 'jakarta.servlet:jakarta.servlet-api:6.0.0' } // 首页位置 - src/main/webapp/index.html // http://localhost:8080/app/xx/any //@MultipartConfig(maxFileSize = 1024 * 1024 * 10) // form上传模式enctype="multipart/form-data"; 注意 - GAE getParts()时报"Error: bad multipart"可用commons-fileupload2-jakarta-servlet6库 @WebServlet(urlPatterns = {"/x", "/xx/*"}) // urlPatterns等同value = {"/y", "/yy/*"},但两者互斥。 public class NewClass extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try ( var out = response.getWriter()) { out.println(request.getServletPath()); } } }

 

Servlet异步非阻塞:

// http://localhost:8080/gradle-servlet-template/aa/any // 若配置了 loadOnStartup = 1 则也必须配置 value = "/must-be-specified",web.xml则为可选。 @WebServlet(asyncSupported = true, urlPatterns = {"/a", "/a/*"}) public class NewServlet extends HttpServlet { private static final ExecutorService ES = Executors.newFixedThreadPool(9); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 或指定非当前Servlet的请求响应入参;注意 - GAE不支持无入参的startAsync()。 var ac = request.startAsync(); // request.startAsync(request, response); ac.setTimeout(10 * 1000); // ac.start(() -> {...}); // 或 new Thread(() -> {...}).start(); // 线程利用率无提升,应换用线程池: ES.execute(() -> { try { TimeUnit.SECONDS.sleep(2); // 模拟业务耗时 // 响应JSON对象字符串: implementation 'org.glassfish:jakarta.json:2.0.1' var wf = Json.createWriterFactory(Map.of( JsonGenerator.PRETTY_PRINTING, true)); var sw = new StringWriter(); try (var jw = wf.createWriter(sw)) { var jo = Json.createObjectBuilder() .add("ok", true) .build(); jw.writeObject(jo); } //response.setContentType("application/json"); response.getWriter().print(sw.toString()); response.getWriter().flush(); ac.getResponse().getWriter().write(sw.toString()); ac.complete(); // 通知异步上下文请求处理完成 } catch (IOException | InterruptedException e) { System.err.println(e); } }); } }
// http://localhost:8080/gradle-servlet-template/aa/any @WebServlet(asyncSupported = true, urlPatterns = {"/a", "/a/*"}) public class NewServlet extends HttpServlet { private static final ExecutorService ES = Executors.newFixedThreadPool(9); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (request.getDispatcherType() == DispatcherType.ASYNC) { response.getWriter().write("from ac.dispatch()."); } else { var ac = request.isAsyncStarted() ? request.getAsyncContext() : request.startAsync(request, response); ac.setTimeout(10 * 1000); //ac.addListener(new AsyncListener() {}); // 异步非阻塞方式读取或响应HTTP Body大尺寸内容 //var is = request.getInputStream(); //is.setReadListener(new MyRL(is, asyncContext)); //var os = response.getOutputStream(); //os.setWriteListener(new MyWL(os, asyncContext)); // 若业务线程需要Body参数可在MyRL的onAllDataRead()回调中拿取 // new Thread(() -> {...}).start(); // 线程利用率无提升,应换用线程池: ES.execute(() -> { // 或 ac.start(() -> {...}); try { TimeUnit.SECONDS.sleep(2); // 模拟业务耗时 //if (complete.compareAndSet(false, true)){ ac.complete(); } // 避免已完成。 ac.getResponse().getWriter().write("ok"); ac.complete(); // 通知异步上下文请求处理完成 //ac.dispatch(); // 或转发至当前Servlet } catch (IOException | InterruptedException e) { System.err.println(e); } }); } } }

 

    启动 - gradle appRun
注意 - 执行前确保 src\main\webapp\ 网站根目录存在。

  extends HttpServlet的doGet(...)方法并删掉IDE生成的响应了SC_METHOD_NOT_ALLOWED的super.doGet(req, resp);
  输出文本 - response.getWriter().append("hi!");
  响应状态- resp.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); resp.addHeader("Location", "/"); // 永久转向用SC_MOVED_PERMANENTLY
  转向语句r.sendRedirect(path)会附加全URL至302临时转向的Location头

JAX-RS(Java API for RESTful Web Services)

新:
  Quarkus 尝试下 @org.jboss.resteasy.reactive.NoCache

死记:
  接收前端FormData参数时,Quarkus只能用非注解入参multipartFormDataInput.getFormDataMap().get("id").getFirst().getBodyAsString(),而用不了Open Liberty支持的@FormData。

综合:
  查询字符串入参用 @QueryParam("qp") String qp。

全局异常处理:
  粗粒度整站全局异常处理:throw new jakarta.ws.rs.WebApplicationException(Response.status(Response.Status.NOT_FOUND).build());
  细粒度按异常类逐个处理:
    @jakarta.ws.rs.ext.Provider // 自动发现;或处理部分异常 jakarta.ws.rs.ext.ExceptionMapper<OtherException>
    public class GlobalExceptionMapper implements jakarta.ws.rs.ext.ExceptionMapper<Throwable> {
      @Override public Response toResponse(Throwable e) { return Response.serverError().build(); }
    }

    // 用法实例:
    @Path("items/{id}") public class Endpoint {
      @GET public String find(String id) {
        if(id == null)
            throw new BadRequestException(); // HTTP 400
        if(!id.equals("idValue"))
            throw new jakarta.ws.rs.NotFoundException("Unknown object: " + id); // HTTP 404
        return "Found - " + id;
      }
    }

  quarkus要想全权交予com.fasterxml.jackson.databind.JsonMappingException处理,则应移除内置的JSON异常包装类:
    quarkus.class-loading.removed-resources."io.quarkus\:quarkus-rest-jackson"=io/quarkus/resteasy/reactive/jackson/runtime/mappers/BuiltinMismatchedInputExceptionMapper.class

注入Servlet对象:@Context private HttpServletRequest request;

响应返回值:
  说明 - 
    return Response.ok().build() 比 return "非泛型对象" 更细节可控;Response.ok(null)返回“{}”,入参new ArrayList()则返回“[]”。
    默认响应HTML,其他MIME则必须写注解 @Produces(MediaType.APPLICATION_JSON) 或 Response.ok().type(MediaType.TEXT_PLAIN).build()
    抛异常 - throw new WebApplicationException(Response.Status.NOT_FOUND); 或 @Provider ... implements ExceptionMapper


    @GET
    @Path("http-204-no-content")
    public void void204() { // HTTP/1.1 204 No Content
        System.out.println("等同 return null; 或 return Response.noContent().build();");
    }

    @GET
    @Path("http-200")
    @Produces(MediaType.APPLICATION_JSON)
    public Response http200() {
        return Response.ok().build() // 返回http 200但无body; 或 return Response.ok("body对象").build()
        //return Response.ok(new GenericEntity<List<String>>( List.of("解决泛型擦除问题") ){}).build();
    }
    public record ResultRecord(String text) { } // 支持响应record对象


    @PUT
    @Path("upload")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response upload(@FormParam("uploadedFile") EntityPart uf) {
        System.out.println(uf.getFileName().orElse(null));

        // 或 fis.transferTo(os); 通常搭配fetch处理进度。
        // 底层基于缓冲型的Transfer-Encoding: chunked,会忽略a链接download属性。
        var bytes = "xyz".getBytes();
        if(bytes != null){
          StreamingOutput so = (os) -> { try (os) { os.write(bytes); } };
          return Response.ok(so).build();
        }else { return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); }

        // Quarkus则必须用不注解的(MultipartFormDataInput uf)入参:
        //System.out.println(uf.getFormDataMap().get("uploadedFile").getFirst());

        // 让浏览器下载另存为(若设定了mimeType则无需@Produces注解):
        //return Response.ok((Object) new File("x.txt"))
        //  .header("Content-Disposition", "attachment; filename=\"download.txt\"").type(MediaType.TEXT_PLAIN).build();
    }


JSF

JSF页面JavaScript脚本:
  如果未用CDATA括住,单引号括双引号正常,但双引号括单引号时,则会全部变成双引号,且遇到 & 时报错:“实体名称必须紧跟在 '&' 后面”。
  若括住则不对引号的单双做改变,且不用转义 & 字符:
    <script>//<![CDATA[
      console.log("双引号内的'单引号'和&符号均正常");
    //]]></script

  JSF大部分情况下,只需对小于号转义: 将 < 写为 &lt;
  三元表达式避免JSF转义写法: if (location.hostname !== "localhost" ? location.hostname !== "127.0.0.1" : false) { }
  转义终极解决办法 - 将脚本独立为x.js文件!
  或 x.innerHTML = `<a target='_blank' href='https://example.com/path/` + x.id + `'>` + x.name + `</a>`;
  转义用 HTML编辑器

Expression Language (EL) 前缀 & 的实体引用必须以分号(;)结尾,否则报“实体名称必须紧跟在 '&' 后面”;
  符号&自身的实体引用写为“&amp;”,比如 <a href="https://example.com/?k=v&amp;kk=vv" target="_blank">含&链接</a>。
  若用到 && 符号,则可换为单词 and 替代:
    <ui:fragment rendered="#{!request.getServerName().equals('localhost') and request.getHeader('X-Real-IP') != null}">x</ui:fragment>

表达式语法:
  #{info.x != null and !info.x.isBlank()}

JSF指定内容类型:  比如将 text/html 改为文本型以避免抓取,或直接用 Filter 加头 X-Robots-Tag: noindex。
JSF响应404状态码:
  var ec = fc.getExternalContext();
  if (!ec.getRequestServerName().startsWith("nav.congci.com")) {
    ec.responseSendError(404, "404."); fc.responseComplete();
    return;
  }

JSF结合HTML标签:
  <ui:composition xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
    ui:composition 最终输出不会自动添加 <html><body></body></html> 标签
    <h:outputText value = "Hi!" />
  </ui:composition>

  #{x}默认已转义:<meta name="author" content="#{homepageView.testString()}" /> 输出 <meta name="author" content="&lt;hr /&gt;" />

  quarkus:
    声明 xmlns:faces="jakarta.faces" xmlns:pt="jakarta.faces.passthrough"
    亲测可用 <h:button pt:title="#{homepageView.testString()}" />
    亲测可用 <h:inputText><f:passThroughAttribute name="value" value="#{homepageView.testString()}" /></h:inputText>
    亲测没用 <input faces:value="#{homepageView.testString()}" />

JSF Beans:
  #{jsfSingleton.env('TMP')} 或起个变量名 <ui:param name="varName" value="#{jsfSingleton.env('TMP')}" />
@Named @RequestScoped // @Singleton 在 Open Liberty 的 xhtml 感知不到,Quarkus 下则可用。 public class JsfSingleton { public String env(String name) { return System.getenv(name); } }
JSF遍历: <ui:param name="r" value="#{xView.x(x.id)}" /> <ui:fragment rendered="#{r != null}"> <ul> <ui:repeat value="#{r}" var="item"> <li>#{item.key}:<h:outputText escape="false" value='#{item.value}' /></li> </ui:repeat> </ul> </ui:fragment>

Jakarta EE

Jakarta Data

    @Save // 必须写上注解
    void save(Product p);
其他:无。