15.解决浏览器传递中文问题
由于HTTP协议只支持ISO8859-1, 此字符集不支持中文, 导致请求中包含中文时, 无法正确传递中文. 在UTF-8中每个中文用三个字节表示, 一个方案是在请求前,先将中文按照UTF-8转换为3个字节再传递, 比如若是想要传输姓名,则需要24个字节, 带来的问题,数据量大传递的速度慢,而且太冗长 .
另外一种方案, 使用十六进制简化二进制表达! 此时带来另外问题:如何与实际的英文数字组合区分呢?方案:如果想表示16进制内容而不是英文, 每两位16进制前使用%
最终方案:在HttpServletRequest类中的parseParameters()方法中先对请求路径中的中文部分使用API URLDecode(line, "UTF-8")转换为中文
拓展知识: 有Unicode编码为什么需要UTF-8?
例子:QQ聊天中,A传了一个中文给B, 由于使用Unicode是一个中文用两个字节表达,一个英文/符号用一个字节表达, 计算机底层在传输过程中底层传输的都是二进制字节, 那么B应该把这两个字节当成两个英文或符号还是中文,使用UTF-8则解决了这一问题
private void parseParameters(String line){
//先将参数转换为中文
try {
line = URLDecoder.decode(line,"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String[] data = line.split("&");//将参数部分按照"&"拆分出每一组参数
for(String para : data){
//para: username=zhangsan
String[] paras = para.split("=");
parameters.put(paras[0],paras.length>1?paras[1]:"");
}
}
16.实现处理动态页面
原方案:在前端控制器类中设置/userList映射处理, 在处理器中生成一个userList.html响应给浏览器 这种方案存在并发安全问题和性能问题
- 涉及到硬盘的读文件和写文件,有性能问题
- 多个浏览器同时对userList.html进行写操作,线程不安全
解决方案: 使用ByteArrayOutputStream存储动态数据,并且以此数据是否为null,进行sendContent()方法重构,新增 getOutputStream方法和getWriter()方法,setContentType()方法
此需求即请求是不是一个业务处理,若不是业务处理(即ByteArrayOutputStream为null
)而是请求资源是否存在对应资源,若有,则设置好响应的静态资源,若无则重定向至404
public class HttpServletResponse {
private Socket socket;
//状态行的相关信息
private int statusCode = 200; //状态代码
private String statusReason = "OK"; //状态描述
//响应头相关信息
private Map<String,String> headers = new HashMap<>(); //响应头
//响应正文相关信息
private File contentFile; //正文对应的实体文件
private ByteArrayOutputStream baos;
public HttpServletResponse(Socket socket){
this.socket = socket;
}
/**
* 将当前响应对象内容以标准的HTTP响应格式,发送给客户端(浏览器)
*/
public void response() throws IOException {
//发送前的准备工作
sendBefore();
//3.1发送状态行
sendStatusLine();
//3.2发送响应头
sendHeaders();
//3.3发送响应正文
sendContent();
}
//发送前的准备工作
private void sendBefore(){
//判断是否有动态数据存在
if(baos!=null){
addHeader("Content-Length",baos.size()+"");
}
}
//发送状态行
private void sendStatusLine() throws IOException {
println("HTTP/1.1" + " " + statusCode + " " +statusReason);
}
//发送响应头
private void sendHeaders() throws IOException {
/*
当发送响应头时,所有待发送的都应当都作为键值对存入了headers中
headers:
key value
Content-Type text/html
Content-Length 245
Server WebServer
... ...
*/
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for(Map.Entry<String,String> e : entrySet){
String name = e.getKey();
String value = e.getValue();
println(name + ": " + value);
}
//单独发送一组回车+换行表示响应头部分发送完了!
println("");
}
//发送响应正文
private void sendContent() throws IOException {
if(baos!=null){//存在动态数据
byte[] data = baos.toByteArray();
OutputStream out = socket.getOutputStream();
out.write(data);
}else if(contentFile!=null) {
try (
FileInputStream fis = new FileInputStream(contentFile);
) {
OutputStream out = socket.getOutputStream();
int len;
byte[] data = new byte[1024 * 10];
while ((len = fis.read(data)) != -1) {
out.write(data, 0, len);
}
}
}
}
public void sendRedirect(String path){
statusCode = 302;
statusReason = "Moved Temporarily";
addHeader("Location",path);
}
/**
* 向浏览器发送一行字符串(自动补充CR+LF)
* @param line
*/
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
out.write(line.getBytes(StandardCharsets.ISO_8859_1));
out.write(13);//发送回车符
out.write(10);//发送换行符
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getStatusReason() {
return statusReason;
}
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
public File getContentFile() {
return contentFile;
}
public void setContentFile(File contentFile) throws IOException {
this.contentFile = contentFile;
String contentType = Files.probeContentType(contentFile.toPath());
//如果根据文件没有分析出Content-Type的值就不添加这个头了,HTTP协议规定服务端不发送这个头时由浏览器自行判断类型
if(contentType!=null){
addHeader("Content-Type",contentType);
}
addHeader("Content-Length",contentFile.length()+"");
}
public void addHeader(String name,String value){
headers.put(name,value);
}
/**
* 通过返回的字节输出流写出的字节最终会作为响应正文内容发送给客户端
* @return
*/
public OutputStream getOutputStream(){
if(baos==null){
baos = new ByteArrayOutputStream();
}
return baos;
}
public PrintWriter getWriter(){
OutputStream out = getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw,true);
return pw;
}
/**
* 添加响应头Content-Type
* @param mime
*/
public void setContentType(String mime){
addHeader("Content-Type",mime);
}
}
17.利用反射注解机制实现处理请求
当我们得到本次请求路径path的值后,我们首先要查看是否为请求业务:
- 扫描controller包下的所有类
- 查看哪些被注解@Controller标注的过的类(只有被该注解标注的类才认可为业务处理类)
- 遍历这些类,并获取他们的所有方法,并查看哪些时业务方法只有被注解@RequestMapping标注的方法才是业务方法
- 遍历业务方法时比对该方法上@RequestMapping中传递的参数值是否与本次请求路径path值一致?如果一致则说明本次请求就应当由该方法进行处理,因此利用反射机制调用该方法进行处理。
- 如果扫描了所有的Controller中所有的业务方法,均未找到与本次请求匹配的路径
则说明本次请求并非处理业务,那么执行下面请求静态资源的操作
public class DispatcherServlet {
private static DispatcherServlet servlet;
private static File rootDir;
private static File staticDir;
static {
servlet = new DispatcherServlet();
try {
//定位到:target/classes
rootDir = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
//定位static目录
staticDir = new File(rootDir, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet() {
}
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
//不能直接使用uri作为请求路径处理了,因为可能包含参数,而参数内容不是固定信息。
String path = request.getRequestURI();
//判断本次请求是否为请求某个业务
try {
File dir = new File(rootDir,"com/webserver/controller");
File[] subs = dir.listFiles(f->f.getName().endsWith(".class"));
for(File sub : subs){
String fileName = sub.getName();
String className = fileName.substring(0,fileName.indexOf("."));
className = "com.webserver.controller."+className;
Class cls = Class.forName(className);
if(cls.isAnnotationPresent(Controller.class)){
Object obj = cls.newInstance();//实例化这个Controller
Method[] methods = cls.getDeclaredMethods();
for(Method method : methods){
if(method.isAnnotationPresent(RequestMapping.class)){
RequestMapping rm = method.getAnnotation(RequestMapping.class);
String value = rm.value();
if(path.equals(value)){//如果请求路径与该方法中@RequestMapping注解的参数值一致
method.invoke(obj,request,response);
return;//业务处理完直接接触处理请求的操作(不走下面的处理静态页面操作)
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
File file = new File(staticDir, path);
System.out.println("该页面是否存在:" + file.exists());
if (file.isFile()) {//用户请求的资源在static目录下存在且是一个文件
response.setContentFile(file);
} else {
response.setStatusCode(404);
response.setStatusReason("NotFound");
response.setContentFile(new File(staticDir, "/root/404.html"));
}
//测试添加其它响应头
response.addHeader("Server", "WebServer");
}
public static DispatcherServlet getInstance() {
return servlet;
}
}
17.重构DispatcherServlet
将判断本次请求是否为请求某个业务的代码提取到新建的类HandllerMapping中, 实现解耦合
public class HandlerMapping {
private static Map<String, Method> mapping = new HashMap<>();
static{
init();
}
private static void init(){
try {
File dir = new File(
HandlerMapping.class.getClassLoader().getResource(
"./com/webserver/controller"
).toURI()
);
File[] subs = dir.listFiles(f->f.getName().endsWith(".class"));
for(File sub : subs){
String fileName = sub.getName();
String className = fileName.substring(0,fileName.indexOf("."));
className = "com.webserver.controller."+className;
Class cls = Class.forName(className);
if(cls.isAnnotationPresent(Controller.class)){
// Object obj = cls.newInstance();//实例化这个Controller
Method[] methods = cls.getDeclaredMethods();
for(Method method : methods){
method.getDeclaringClass();
if(method.isAnnotationPresent(RequestMapping.class)){
RequestMapping rm = method.getAnnotation(RequestMapping.class);
String value = rm.value();
mapping.put(value,method);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据请求路径获取处理该请求的某Controller的对应方法
* @param path
* @return
*/
public static Method getMethod(String path){
return mapping.get(path);
}
DispatchServlet
public class DispatcherServlet {
private static DispatcherServlet servlet;
private static File rootDir;
private static File staticDir;
static {
servlet = new DispatcherServlet();
try {
//定位到:target/classes
rootDir = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
//定位static目录
staticDir = new File(rootDir, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet() {
}
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
//不能直接使用uri作为请求路径处理了,因为可能包含参数,而参数内容不是固定信息。
String path = request.getRequestURI();
//根据请求路径判断是否为处理某个业务
try {
Method method = HandlerMapping.getMethod(path);//path:/userList
if(method!=null){
method.invoke(method.getDeclaringClass().newInstance(),request,response);
return;
}
} catch (Exception e) {
e.printStackTrace();
}
File file = new File(staticDir, path);
System.out.println("该页面是否存在:" + file.exists());
if (file.isFile()) {//用户请求的资源在static目录下存在且是一个文件
response.setContentFile(file);
} else {
response.setStatusCode(404);
response.setStatusReason("NotFound");
response.setContentFile(new File(staticDir, "/root/404.html"));
}
//测试添加其它响应头
response.addHeader("Server", "WebServer");
}
public static DispatcherServlet getInstance() {
return servlet;
}
}