第二天
HTTP协议类比TCP/IP协议是需要握手的
Request&Response
Request(请求) & Response(响应)
浏览器向服务器请求,服务器对浏览器做出回应
Request:获取请求数据
Response:设置响应数据
Request
Request 继承体系:
ServletRequest:Java提供的请求对象根接口
|
HttpServletRequest:Java提供的对Http协议封装的请求对象接口
|
RequestFacade:Tomcat定义的实现类
1、Tomcat需要解析数据,封装为request对象,并且创建request对象传递到service方法中
2、使用request对象,查询JavaEE API文档的HttpServletRequest接口
Request 获取请求数据
获取请求数据:
请求数据分为3部分:
1、请求行:
GET/request-demo/req1?username=zhangsan HTTP/1.1
请求方式 + 请求的资源路径和参数 + 协议和版本号
?前面是URL
?后面是GET的请求参数
常用方法 | 作用 |
---|---|
String getMethod() | 获取请求方式:GET |
String getContextPath() | 获取虚拟目录(项目访问路径):/request-demo |
StringBuffer getRequestURL() | 获取URL(统一资源定位符):http://localhost:8080/request-demo/req1 |
String getRequestURI() | 获取URI(统一资源标识符):/request-demo/req1 |
String getQueryString() | 获取请求参数(GET方式):username=zhangsan&password=123 |
参考代码:
需要使用maven项目,同时配置好Tomcat
添加的pom配置:
<!-- 选择打包方式 -->
<packaging>war</packaging>
<!-- 配置字符集和JDK -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<build>
<plugins>
<!-- 配置Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<!--
配置项目访问路径
-->
<!--<configuration>
<path>/xxx</path>
</configuration>-->
</plugin>
</plugins>
</build>
<dependencies>
<!-- 导入servlet依赖坐标 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<!-- 依赖范围:编译,测试有效,运行无效(Tomcat自带,运行环境不用打入war包这样不会冲突) -->
<scope>provided</scope>
</dependency>
</dependencies>
package com.itheima.web;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/req1")
public class RequestDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//String getMethod()获取请求方式:GET
String method = req.getMethod();
System.out.println(method); //GET
//String getContextPath()获取虚拟目录(项目访问路径):/request-demo
String contextPath = req.getContextPath();
System.out.println(contextPath); //未在pom中设置时默认模块名称
//StringBuffer getRequestURL()获取URL(统一资源定位符):http://localhost:8080/request-demo/req1
StringBuffer url = req.getRequestURL();
System.out.println(url.toString());
//String getRequestURl()获取URl(统一资源标识符):/request-demo/req1
String uri = req.getRequestURI();
System.out.println(uri);
//String getQueryString()获取请求参数(GET方式):username=zhangsan&password=123
String queryString = req.getQueryString();
System.out.println(queryString);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
2、请求头:
User-Agent: Mozilla/5.0 Chrome/91.0.4472.106
键:值
String getHeader(String name):根据请求头名称,获取值
3、请求体(只有post请求才会有请求体):
username=superbaby&password=123
ServletInputStream getInputStream():获取字节输入流
BufferedReader getReader():获取字符输入流
通用方式获取请求参数:
请求参数获取方式:
GET方式:
String getQueryString();
POST方式:
BufferedReader getReader();
思考:
GET请求方式 和 POST请求方式 区别主要在于获取请求参数的方式不一样,是否可以提供一种统一获取请求参数的方式,从而统一doGet 和 doPost方法内的代码呢?
//伪代码:
//获取请求方式
String method = this.getMethod();
//判断
if("GET".equals(method)){
//GET方式获取请求参数
params = this.getQueryString();
}else if("POST".equals(method)){
//POST方式获取请求参数
BufferedReader reader = this.getReader();
params = reader.readLine();
}
Map<String, String[]> //通过Map集合存储参数
//考虑到键相同时,值被覆盖,所以使用String[]存放值,当键相同时可以存放在值所对应的数组后
通用的方法:
Map<String, String[]> getParameterMap():获取所有参数Map集合
String[] getParameterValues(String name):根据名称获取参数值(数组)
String getParameter(String name):根据名称获取参数值(单个值)
参考代码:
方法中的内容一致后,直接在另一个方法中this.xxx
package com.itheima.web;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
@WebServlet("/req2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//GET请求数据
System.out.println("get...");
//1、获取所有参数的map集合
Map<String, String[]> map = req.getParameterMap();
for(String key: map.keySet()){ //通过keySet()获取键值
//username:zhangsan
System.out.print(key + ":");
//获取值
String[] values = map.get(key); //通过get(key)方法获取对应值
for(String value : values){
System.out.print(value + " ");
}
System.out.println();
}
System.out.println("--------------------------------");
//2、获取对应参数值,数组
String[] hobbies = req.getParameterValues("hobby");
for(String hobby : hobbies){
System.out.println(hobby);
}
System.out.println("--------------------------------");
//3、根据key,获取单个参数值
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println(username);
System.out.println(password);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//POST请求数据
this.doGet(req, resp);
/*
System.out.println("post...");
//1、获取所有参数的map集合
Map<String, String[]> map = req.getParameterMap();
for(String key: map.keySet()){ //通过keySet()获取键值
//username:zhangsan
System.out.print(key + ":");
//获取值
String[] values = map.get(key); //通过get(key)方法获取对应值
for(String value : values){
System.out.print(value + " ");
}
System.out.println();
}
System.out.println("--------------------------------");
//2、获取对应参数值,数组
String[] hobbies = req.getParameterValues("hobby");
for(String hobby : hobbies){
System.out.println(hobby);
}
System.out.println("--------------------------------");
//3、根据key,获取单个参数值
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println(username);
System.out.println(password);
*/
}
}
使用通用方式获取请求参数后,屏蔽了GET和POST的请求方式代码的不同,则代码可以定义为如下格式:
@WebServlet("/req2")
public class RequestDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
可以使用Servlet模板创建Servlet更高效,模板可以自定设置
方法直接在对应位置new一下,如果没有看到尝试一下这个设置
请求参数中文乱码
请求参数中如果存在中文数据,则会乱码
解决方案:
POST:设置输入流的编码
request.setCharacterEncoding("UTF-8"); //根据页面设置字符输入流的编码
浏览器将中文转换为URL编码(UTF-8),Tomcat进行解码(ISO-8859-1)
即先将中文进行UTF-8或者ISO-8859-1编码,在此基础上进行URL编码
URL编码(% + 两位16进制数 为一个字节):
1、将字符串按照编码方式转换为二进制
2、每一个字节转为2个16进制数并在前面加上%
UTF-8下一个汉字三个字节
GET乱码分析:
package com.itheima.web;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
//Java提供的URL编码解码工具类模拟编码解码
public class URLDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
String username = "张三";
//1、URL编码 (浏览器)
String encode = URLEncoder.encode(username, "UTF-8");
System.out.println(encode);
//2、URL解码 (Tomcat)
//String decode = URLDecoder.decode(encode, "UTF-8");
String decode = URLDecoder.decode(encode, "ISO-8859-1");
System.out.println(decode);
//3、解决乱码:(我们解决)
//将字节数据转换为,字符串(编码)
byte[] bytes = decode.getBytes("ISO-8859-1");
/*for(byte b : bytes){
System.out.print(b + " ");
}*/
//将字节数组转为字符串(解码)
String s = new String(bytes, "UTF-8");
System.out.println(s);
}
}
关键代码:
//username是Tomcat解码后获取的乱码数据
//先通过对应Tomcat默认字符集将其转化为二进制数组 , 然后通过合适字符集转化为字符串
username = new String(username.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
Tomcat 8.0之后,已将GET请求乱码问题解决,设置默认的解码方式为UTF-8(使用的插件只支持到7,可以打个war包去本地的Tomcat中去试试)
Request 请求转发
请求转发(forward):一种在服务器内部的资源跳转方式
实现方式:
req.getRequestDispatcher("资源B路径").forward(req,resp); //传递数据,并没有进行处理,相当于传送带
请求转发资源间共享数据:使用Request对象
(即通过这些方法处理传递的数据,相当于加工工具)
void setAttribute(String name, Object o):存储数据到 request域(作用域,范围的意思)中
Object getAttribute(String name):根据 key,获取值
void removeAttribute(String name):根据 key,删除该键值对
请求转发的特点:
浏览器地址栏路径不发生变化
只能转发到当前服务器的内部资源
一次请求,可以在转发的资源间使用request共享数据
参考代码:
package com.itheima.web;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/*
请求转发
*/
@WebServlet("/req5")
public class RequestDemo5 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo5");
//存储数据:
request.setAttribute("msg", "hello");
//请求转发
request.getRequestDispatcher("/req6").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
package com.itheima.web;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*
请求转发
*/
@WebServlet("/req6")
public class RequestDemo6 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo6");
//获取数据:
Object msg = request.getAttribute("msg");
System.out.println(msg.toString());
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
Respeonse
继承体系:
ServletResponse:Java提供的请求对象根接口
|
HttpServletResponse:Java提供的对Http协议封装的请求对象接口
|
ResponseFacade:Tomcat定义的实现类
Response 设置响应数据功能介绍
响应数据分为3部分:
1、响应行:
HTTP/1.1 200 OK
void setStatus(int sc):设置响应状态码
2、响应头:
Content-Type:text/html
void setHeader(String name, String value):设置响应头键值对
3、响应体:
<html><head></head><body></body></html>
PrintWriter getWriter():获取字符输出流
ServeltOutputStream getOutputStream():获取字节输出流
Response 完成重定向
重定向(Redirect):一种资源跳转方式
当浏览器向资源A做出请求时,资源A向浏览器响应自己无法处理(响应状态码302),并告诉浏览器(响应头location:xxx)资源B可以解决,这是浏览器收到响应并向资源B做出请求。
实现方式:
resp.setStatus(302);
resp.setHeader("location", "资源B的路径");
简化实现方式(只需要输入变化的东内容,固定的状态码和location底层自动输入):
response.sendRedirect("/request-demo/resp2");
重定向特点(和资源转发进行对比):
两者本质区别在于,浏览器是否重新发送请求
浏览器地址栏路径发生变化(浏览器进行了多次请求,资源转发并没有)
可以重定向到任意位置的资源(服务器内部,外部均可,这是因为重定向浏览器会重新请求,这时请求的对象不一定还是服务器)
两次请求,不能在多个资源使用request共享数据(两次是不同的request作用域)
参考代码:
package com.itheima.web.response;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//重定向
@WebServlet("/resp1")
public class ResponseDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1...");
//重定向
/* //1、设置响应状态码302
response.setStatus(302);
//2、设置响应头:location
response.setHeader("Location", "/request-demo/resp2");*/
//简化方式完成重定向
//虚拟目录可以在 pom中配置,一改代码也要改,所以提供了动态获取
String contextPath = request.getContextPath();
//由于代码的固定性(302,Location),变的只有路径,所以直接提供了一个简化方法,底层进行此操作
response.sendRedirect(contextPath + "/resp2");
// response.sendRedirect("http://www.itcast.cn");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
package com.itheima.web.response;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//重定向
@WebServlet("/resp2")
public class ResponseDemo2 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp2...");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
路径:
先明确路径谁使用?
浏览器使用:需要加上虚拟目录(项目访问路径)
服务端使用:不需要加虚拟目录
练习:
<a href='路径'> //标签a:超链接,浏览器使用需要加虚拟目录
<from action='路径'> //表单:浏览器使用需要加虚拟目录
req.getRequestDispatcher("路径") //服务器内部进行资源跳转,不需要加虚拟目录
resp.sendRedirect("路径") //浏览器使用需要加虚拟目录
Response 响应字符数据
使用:
1、通过Response对象获取字符输出流
PrintWriter writer = resp.getWriter(); //由对象创建的流,随着对象的销毁自动释放资源
2、写数据:
writer.writer("aaa");
写入数据非纯文本时:
//content-Type 表示该响应内容的类型,例如text/html,image/jpeg;
//通过方法设置:content-type
写入数据含中文时:
//先设置流的编码
response.setContentType("text/html;charset=UTF-8");
//前面是响应数据类型 ; 后面是编码字符集
参考代码:
package com.itheima.web.response;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
//响应字符数据:设置字符数据的响应体
@WebServlet("/resp3")
public class ResponseDemo3 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置流的编码
response.setContentType("text/html;charset=UTF-8");
//1、获取字符输出流
PrintWriter writer = response.getWriter();
//设置content-type 就是告诉浏览器我们写的数据类型,不告诉就默认为存文本
// response.setHeader("content-type", "text/html");
writer.write("你好");
writer.write("<h1>aaa</h1>");
//这个流是response对象获取的,当这个对象被销毁时,会自动释放资源
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
Reponse 响应字节数据
使用:
1、通过Response对象获取字符输出流:
ServeletOutputStream outputStream = resp.getOutputStream();
2、写数据:
outputStream.write(字节数据);
使用IOUtils工具类来简化复制:
1、导入坐标:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
2、使用:
IOUtils.copy(输入流, 输出流);
案例:用户登录
流程说明:
1、用户填写用户名,密码,提交到LoginServlet
2、在LoginServlet中使用MyBatis查询数据库,验证用户名密码是否正确
3、如果正确,响应”登录成功“,如果错误,响应”登录失败“
需要一个登录页面
需要一个登录的Servlet类来接收数据,做出响应
需要一个连接MyBatis类
准备环境:
1、准备一个静态页面代码到项目的webapp目录下
2、创建db1数据库,创建tb_user表,创建User实体类
3、导入Mybatis坐标,MySQL驱动坐标
4、创建mybatis-config.xml核心配置文件,UserMapper.xml映射文件,UserMapper接口
参考代码:
package com.itheima.web;
import com.itheima.mapper.UserMapper;
import com.itheima.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
@WebServlet("/loginServlet")
public class LoginServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1、接收用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
//2、调用MyBatis完成查询
//2.1 获取SqlSessionFactory对象
String resource = "mybatis-config.xml"; //位置在resources根目录下
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.2 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//2.3 获取Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//2.4 调用方法
User user = userMapper.select(username, password);
//2.5 释放资源
sqlSession.close();
//含有中文数据,先设置content-type 和 字符集
response.setContentType("text/html;charset=UTF-8");
//先获取响应输出流
PrintWriter writer = response.getWriter();
//3、判断user是否为null
if(user != null){
//登录成功
writer.write("登录成功");
}else{
//登录失败
writer.write("登录失败");
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
案例:用户注册
流程说明:
1、用户填写用户名、密码等信息,点击注册按钮,提交到RegisterServlet
2、在RegisterServlet中使用MyBatis保存数据
3、保存前需要判断用户名是否已经存在:根据用户查询数据库
接收用户数据
调用查询方法,看是否用户名已经存在
判断返回结果,存在返回提示,不存在添加
参考代码:
package com.itheima.web;
import com.itheima.mapper.UserMapper;
import com.itheima.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
@WebServlet("/registerServlet")
public class RegisterServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1、接收注册用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
//封装用户对象
User user = new User();
user.setUsername(username);
user.setPassword(password);
//2、调用mapper,根据用户名查询用户对象
//2.1 获取SqlSessionFactory对象
String resource = "mybatis-config.xml"; //位置在resources根目录下
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.2 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//2.3 获取Mapper
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//2.4 调用方法
User u = userMapper.selectByUsername(username); //接收返回值
//3、判断查询的用户对象是否为null,即注册的用户名是否已经存在
if(u == null){
//用户名不存在,添加用户
userMapper.add(user);
//!!!注意(增删改):需要提交事务
//提交事务
sqlSession.commit();
//2.5 释放资源
sqlSession.close();
}
else{
//用户名已存在,提示不能创建
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("用户名已存在");
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
代码优化:
创建SqlSessionFactory代码优化
//2.1 获取SqlSessionFactory对象
String resource = "mybatis-config.xml"; //位置在resources根目录下
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
问题:
1、代码重复:写一个工具类
2、SqlSessionFactory工厂只创建一次,不要重复创建(连接池多个,消耗资源):静态代码块
参考代码:
package com.itheima.util;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
//获取SqlSessionFactory对象的工具类
public class SqlSessionFactoryUtils {
//提升作用域:
private static SqlSessionFactory sqlSessionFactory;
//静态代码块的代码,会随着类的加载而自动执行,且只会执行一次
//不能抛出异常,使用try...catch
static {
try {
String resource = "mybatis-config.xml"; //位置在resources根目录下
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSessionFactory getSqlSessionFactory(){
return sqlSessionFactory;
}
}
注意sqlSession不能如此,两者不同。
sqlSessionFactory是一个工厂,为了获取多个sqlSession而创建的,类似数据库连接池,但是用户操作不能并发,修改的同时增加。
sqlSession是用户连接数据库的唯一(针对用户操作)对象,若是增删改查都用一个共有对象处理。。。。。。
标签:String,request,response,第二天,import,servlet,javax From: https://www.cnblogs.com/fragmentary/p/17094949.html