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
JSF(Jakarta Server Faces/Java Server Faces) - https://jakarta.ee/specifications/faces/
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); // 模拟业务耗时
// 响应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大部分情况下,只需对小于号转义: 将 < 写为 <
三元表达式避免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) 前缀 & 的实体引用必须以分号(;)结尾,否则报“实体名称必须紧跟在 '&' 后面”;
符号&自身的实体引用写为“&”,比如 <a href="https://example.com/?k=v&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="<hr />" />
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>
其他:无。