Java Web应用技术
综合
Spring框架 SpringBoot内嵌Web Server容器 - Tomcat,Jetty,Undertow,Netty
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实例:
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大部分情况下,只需对小于号转义: 将 < 写为 < 整块儿转义:
<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) 前缀 & 的实体引用必须以分号(;)结尾,否则报“实体名称必须紧跟在 '&' 后面”;符号&自身的实体引用写为“&”。 若用到 && 符号,则可换为单词 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");