从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

Java Web应用技术


综合

Spring框架

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

Nginx集成Java模块 - nginx-clojure

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

注意 - HttpServletRequest.getInputStream()首次读取正常,再次读取会报 IllegalStateException ,可通过中间变量暂存。
  OpenLiberty 官方称Ubuntu没有计划升级到JDK21 - https://github.com/OpenLiberty/ci.docker/issues/582#issuecomment-2432514946
  OpenLiberty 基于 CRIU 的快速启动容器镜像 - https://openliberty.io/docs/latest/instanton.html

常用:
  request.getParameter("url") 会自动解码 URLEncoder.encode(request.getRequestURL().toString() + "?" + request.getQueryString(), Charset.defaultCharset());

  //  首个空参数 ?& 便于后续只处理&符号。
  var p1="k=v"; "https://example.com/path/?&" + p1 + "&" + p2;

  Cookie - response.addCookie(cv); // 无法被本请求的 request.getCookies() 感知到。
  Session - request.getSession(false) 过期设置 web.xml :
    <web-app ...>
      <session-config><session-timeout>-1</session-timeout><cookie-config><max-age>[默认]浏览器关闭JSESSIONID即失效</max-age></cookie-config></session-config>
    </web-app>
    或 session.setMaxInactiveInterval(30 * 60); // 仅生效在当前请求上下文,单位秒。
JAX-RS解决JSON Binding (JSON-B)报错 - Could not find MessageBodyWriter for response object of type: pkg.JavaRecord$OrPOJO of media type: application/json
  引入库 yasson ;若为 OpenLiberty 则添加 jsonb-3.0

JAX-RS SSE(Server-Sent Events): jakarta.ws.rs.sse.SseEventSink,也能用 Servlet 流式响应 asyncSupported=true 模拟。
  1分20秒重连问题解决 - 开长连接、关闭缓存:(尽量不要在VPN网络下测试)
        location = /main/apis/more/default/ga/anonymous/main/progress/add-video-watermark {
            proxy_http_version 1.1; # 默认的1.0不支持Keep-Alive,而1.1则默认处于长连接。
            proxy_buffering off; # 关闭响应缓存,关闭后会连带proxy_cache也处于off,类似no-cache。
            proxy_set_header Connection '';
            proxy_read_timeout 500;

            proxy_pass http://ip:9080; #  SSE不能用在try_files指令
        }
  必须的响应头:
    Content-Type: text/event-stream
    Cache-Control: no-cache
    Connection: keep-alive

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); // 模拟业务耗时
                                ac.getResponse().getWriter().write("ok");
                                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)

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

响应返回值:
  说明 - 
    return Response.ok().build() 比 return "非泛型对象" 更细节可控。
    默认响应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); 底层基于缓冲型的Transfer-Encoding: chunked
        StreamingOutput so = (os) -> { try (os) { os.write("xyz".getBytes()); } };
        return Response.ok(so).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大部分情况下,只需对小于号转义: 将 < 写为 &lt;
整块儿转义:
    <script>//<![CDATA[
        document.getElementById("x").innerHTML = "<a target='_blank' href='https://example.com/?" + 1 + "'>" + document.title + "</a><br />";
    //]]> console.log("CDATA之外脚本");</script>
转义终极解决办法 - 将脚本独立为x.js文件! 或 x.innerHTML = `<a target='_blank' href='https://example.com/path/` + x.id + `'>` + x.name + `</a>`; 转义用 HTML编辑器 Expression Language (EL) 前缀 & 的实体引用必须以分号(;)结尾,否则报“实体名称必须紧跟在 '&' 后面”;符号&自身的实体引用写为“&amp;”。 若用到 && 符号,则可换为单词 and 替代: <ui:fragment rendered="#{!request.getServerName().equals('localhost') and request.getHeader('X-Real-IP') != null}">x</ui:fragment>

其他:
  解决中文乱码(Java18后则不需要了/默认已是UTF-8) - response.setContentType("text/plain; charset=utf-8");