Java阶段二Day05

Java阶段二Day05

文章目录

  • Java阶段二Day05
    • 截至此版本可实现的流程图为
    • V14
      • UserController
      • ClientHandler
      • DispatcherServlet
      • HttpServletResponse
      • HttpServletRequest
    • V15
      • DispatcherServlet
      • HttpServletResponse
      • HttpServletRequest
    • V16
      • HttpServletRequest
      • HttpServletResponse
    • 反射
      • JAVA反射机制
      • 获取一个类的类对象方式
      • 反射对象

截至此版本可实现的流程图为

在这里插入图片描述

V14

独立完成登录模块

步骤:
1:创建登录相关的页面
1.1:login.html页面,表单要求两个输入框分别为用户名和密码,表单action=“/loginUser”
1.2:login_info_error页面,当用户登录信息输入有误(空着不填等)时提示该页面,页面居中显示一行字:登录信息输入有误,请重新登录
1.3:login_success.html 登录成功提示页面
1.4:login_fail.html 登录失败提示页面

2:在UserController中定义用于处理登录业务的方法login方法定义参考reg方法
3:在DispatcherServlet的判断注册业务下面添加一个else if分支,判断请求路径path的值是否为"/loginUser" 从而调用登录方法处理登录业务

UserController

package com.birdboot.controller;

import com.birdboot.entity.User;
import com.birdboot.http.HttpServletRequest;
import com.birdboot.http.HttpServletResponse;

import java.io.*;

/**
 * 处理与用户相关的业务
 */
public class UserController {
    private static File userDir;//表示存放所有用户信息的目录:users
    static {
        userDir = new File("./users");
        if(!userDir.exists()){
            userDir.mkdirs();
        }
    }

    //处理"/regUser"这个请求
    public void reg(HttpServletRequest request, HttpServletResponse response){
        System.out.println("开始处理用户注册");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String nickname = request.getParameter("nickname");
        String ageStr = request.getParameter("age");
        System.out.println(username+","+password+","+nickname+","+ageStr);
        //必要的验证
        if(username==null||username.isEmpty()||
                password==null||password.isEmpty()||
                nickname==null||nickname.isEmpty()||
                ageStr==null||ageStr.isEmpty()||
                !ageStr.matches("[0-9]+")
        ){
            response.sendRedirect("/reg_info_error.html");
            return;
        }

        int age = Integer.parseInt(ageStr);//将年龄转换为int值
        //2
        User user = new User(username,password,nickname,age);
        /*
            File的构造器
            File(File parent,String child)
            创建一个File对象表示child这个子项,而它是在parent这个File对象所表示的目录中
         */
        File file = new File(userDir,username+".obj");
        if(file.exists()){//注册前发现以该用户名命名的obj文件已经存在,说明是重复用户
            response.sendRedirect("/have_user.html");
            return;
        }

        try (
                FileOutputStream fos = new FileOutputStream(file);
                ObjectOutputStream oos = new ObjectOutputStream(fos);
        ){
            oos.writeObject(user);//保存用户信息完毕

            //3给用户回馈注册成功页面
            //要求浏览器重新访问下述地址对应的页面
            response.sendRedirect("/reg_success.html");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void login(HttpServletRequest request,HttpServletResponse response){
        System.out.println("开始处理登录!!!!");
        //1获取登录信息
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        //必要验证
        if(username==null||username.isEmpty()||password==null||password.isEmpty()){
            response.sendRedirect("/login_info_error.html");
            return;
        }

        //根据该登录用户的名字去定位users下该用户的注册信息
        File file = new File(userDir,username+".obj");
        //判断该文件是否存在,不存在则说明该用户没有注册过
        if(file.exists()){
            //将该用户曾经的注册信息读取出来用于比较密码
            try (
                    FileInputStream fis = new FileInputStream(file);
                    ObjectInputStream ois = new ObjectInputStream(fis);
            ){
                User user = (User)ois.readObject();//读取注册信息
                //比较本次登录的密码是否与注册时该用户输入的密码一致
                if(user.getPassword().equals(password)){
                    //密码一致则登录成功
                    response.sendRedirect("/login_success.html");
                    return;
                }
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

        //如果程序可以执行到这里,则说明要么是用户名没输入对,要么是密码没有输入对.都属于登录失败
        response.sendRedirect("/login_fail.html");

    }
}

ClientHandler

package com.birdboot.core;

import com.birdboot.http.EmptyRequestException;
import com.birdboot.http.HttpServletRequest;
import com.birdboot.http.HttpServletResponse;

import java.io.*;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * 该线程任务负责与指定的客户端进行HTTP交互
 * HTTP协议要求浏览器与服务端采取"一问一答"的模式。对此,这里的处理流程分为三步:
 * 1:解析请求
 * 2:处理请求
 * 3:发送响应
 */
public class ClientHandler implements Runnable {
    private Socket socket;

    public ClientHandler(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        try {
            //1 解析请求
            HttpServletRequest request = new HttpServletRequest(socket);
            HttpServletResponse response = new HttpServletResponse(socket);

            //2 处理请求
            //V8改造:将处理请求的操作移动到DispatcherServlet的service方法中并调用
            DispatcherServlet servlet = new DispatcherServlet();
            servlet.service(request,response);

            //3 发送响应
            response.response();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (EmptyRequestException e) {

        } finally {
            //HTTP协议要求浏览器与服务端交互完毕后要断开连接
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

}

DispatcherServlet

package com.birdboot.core;

import com.birdboot.controller.UserController;
import com.birdboot.http.HttpServletRequest;
import com.birdboot.http.HttpServletResponse;

import java.io.File;
import java.net.URISyntaxException;

/**
 * V8新增内容:
 * 该类是SpringMVC框架与Tomcat整合时的一个关键类
 * Tomcat处理业务原生的都是调用继承了HttpServlet的类来完成,此时需要进行很多配置
 * 以及使用时要作很多重复性劳动。
 * SpringMVC框架提供的该类也是继承了HttpServlet的,使用它来接收处理请求的工作。
 */
public class DispatcherServlet {
    private static File baseDir;//类加载路径
    private static File staticDir;//类加载路径下的static目录

    static{
        try {
            //定位当前项目的类加载路径
            baseDir = new File(
                    DispatcherServlet.class.getClassLoader().getResource(".").toURI()
            );
            //定位类加载路径下的static目录
            staticDir = new File(baseDir, "static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    /**
     * service方法实际上是当我们继承了HttpServlet后必须重写的方法
     * 该方法要求接收两个参数:请求对象与响应对象。
     * Tomcat在处理请求时就是调用某个Servlet的service方法并将请求与响应对象传入
     * 来让其完成处理工作的。
     */
    public void service(HttpServletRequest request, HttpServletResponse response){
        //获取请求的抽象路径
        //不能在使用uri判断请求了,因为uri可能含参数,内容不固定。
        String path = request.getRequestURI();
        System.out.println(path);

        //判断该请求是否为请求一个业务
        if("/regUser".equals(path)){
            UserController controller = new UserController();
            controller.reg(request, response);
        }else if("/loginUser".equals(path)){
            UserController controller = new UserController();
            controller.login(request, response);
        }else {
            File file = new File(staticDir, path);
            if (file.isFile()) {
                //由于响应对象中状态代码和描述默认值为200,OK因此正确情况下不用再设置
                response.setContentFile(file);
                //设置响应头
                response.addHeader("Server", "BirdServer");
            } else {
                response.setStatusCode(404);
                response.setStatusReason("NotFound");
                file = new File(staticDir, "404.html");
                response.setContentFile(file);
                response.addHeader("Server", "BirdServer");
            }
        }
    }

}

HttpServletResponse

package com.birdboot.http;

import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * V7新增内容:
 * 响应对象
 * 该类的每一个实例用于表示服务端给客户端发送的一个HTTP的响应
 * HTTP协议要求一个响应由三部分构成:状态行,响应头,响应正文
 */
public class HttpServletResponse {

    private static MimetypesFileTypeMap mftm = new MimetypesFileTypeMap();

    private Socket socket;

    //状态行相关信息
    private int statusCode = 200;//状态代码
    private String statusReason = "OK";//状态描述

    //响应头相关信息 key:响应头的名字  value:响应头的值
    private Map<String,String> headers = new HashMap<>();

    //响应正文相关信息
    private File contentFile;//响应正文对应的实体文件


    public HttpServletResponse(Socket socket){
        this.socket = socket;
    }

    /**
     * 该方法用于将当前响应对象内容以标准的HTTP响应格式发送给客户端
     */
    public void response() throws IOException {
        //3.1发送状态行
        sendStatusLine();
        //3.2发送响应头
        sendHeaders();
        //3.3发送响应正文
        sendContent();
    }

    //发送状态行
    private void sendStatusLine() throws IOException {
        println("HTTP/1.1"+" "+statusCode+" "+statusReason);
    }
    //发送响应头
    private void sendHeaders() throws IOException {
      
        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(contentFile!=null) {
            FileInputStream fis = new FileInputStream(contentFile);
            OutputStream out = socket.getOutputStream();
            byte[] buf = new byte[1024 * 10];//10kb
            int d;//记录每次实际读取的数据量
            while ((d = fis.read(buf)) != -1) {
                out.write(buf, 0, d);
            }
        }
    }

    /**
     * V7:将ClientHandler中发送响应的工作全部移动到这里,println方法也是。
     * 向客户端发送一行字符串
     * @param line
     */
    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        out.write(data);
        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;
    }

    /**
     * 设置响应正文对应的实体文件,该方法中会自动根据该文件添加对应的两个响应头:
     * Content-Type和Content-Length
     * @param contentFile
     */
    public void setContentFile(File contentFile) {
        this.contentFile = contentFile;


        addHeader("Content-Type",mftm.getContentType(contentFile));
        addHeader("Content-Length",contentFile.length()+"");
    }

    /**
     * 添加一个响应头
     * @param name
     * @param value
     */
    public void addHeader(String name,String value){
        headers.put(name,value);
    }

    /**
     * 要求浏览器重定向到指定位置
     * @param location
     */
    public void sendRedirect(String location){
        //1设置状态代码302
        statusCode = 302;
        statusReason = "Moved Temporarily";
        //2添加响应头Location
        addHeader("Location",location);
    }
}

HttpServletRequest

package com.birdboot.http;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

/**
 * V4:新增内容
 * 请求对象
 * 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
 * HTTP协议要求请求的格式由三部分构成:请求行,消息头,消息正文
 */
public class HttpServletRequest {
    private Socket socket;
    //请求行相关信息
    private String method;//请求方式
    private String uri;//抽象路径
    private String protocol;//协议版本
    private String requestURI;//保存uri中"?"左侧的请求路径部分
    private String queryString;//保存uri中"?"右侧的参数部分
    private Map<String,String> parameters = new HashMap<>();//保存每一组参数

    //消息头相关信息 key:消息头名字  value:消息头对应的值
    private Map<String,String> headers = new HashMap<>();

    public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
        this.socket = socket;
        //1.1解析请求行
        parseRequestLine();
        //1.2解析消息头
        parseHeaders();
        //1.3解析消息正文
        parseContent();
    }
    //解析请求行
    private void parseRequestLine() throws IOException, EmptyRequestException {
        String line = readLine();

        if(line.isEmpty()){//如果请求行是个空字符串,则说明本次为空请求
            throw new EmptyRequestException();
        }

        System.out.println("请求行:"+line);

        //将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
        String[] data = line.split("\\s");
        method = data[0];
        uri = data[1];
        protocol = data[2];

        parseURI();//进一步解析uri

        System.out.println("method:"+method);
        System.out.println("uri:"+uri);
        System.out.println("protocol:"+protocol);
    }
    //进一步解析uri
    private void parseURI(){
  
        String[] data = uri.split("\\?");
        requestURI = data[0];
        if(data.length>1){
            queryString = data[1];
           
            String[] paraArr = queryString.split("&");
            /*
                再遍历paraArr进一步拆分每一组参数的参数名和参数值
                每一组参数按照"="进行拆分
             */
            //para:username=fancq
            for(String para : paraArr){
                /*
                    username=fancq   用户在浏览器该输入框输入信息
                    username=        用户在浏览器该输入框没有输入信息
                 */
                String[] arr = para.split("=",2);
                parameters.put(arr[0],arr[1]);
            }
        }
        System.out.println("requestURI:"+requestURI);
        System.out.println("queryString:"+queryString);
        System.out.println("parameters:"+parameters);

    }

    //解析消息头
    private void parseHeaders() throws IOException {
        while(true) {
            String line = readLine();
            if(line.isEmpty()){//如果读取到了空行
                break;
            }
            System.out.println("消息头:" + line);
            String[] data = line.split(":\\s");
            headers.put(data[0],data[1]);
        }
        System.out.println("headers:"+headers);
    }
    //解析消息正文
    private void parseContent(){}

    /**
     * 通过socket获取的输入流读取客户端发送过来的一行字符串
     * @return
     */
    private String readLine() throws IOException {//通常被重用的代码不自己处理异常
        //对一个socket实例调用多次getInputStream()返回的始终是同一条输入流。而输出流也是如此
        InputStream in = socket.getInputStream();
        int d;
        char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
        StringBuilder builder = new StringBuilder();//保存读取后的所有字符
        while((d = in.read())!=-1){
            cur = (char)d;//本次读取的字符
            if(pre==13 && cur==10){//是否连续读取到了回车+换行
                break;
            }
            builder.append(cur);//将本次读取的字符拼接
            pre=cur;//在进行下次读取前,将本次读取的字符保存到"上次读取的字符"中
        }
        return builder.toString().trim();
    }

    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }

    public String getProtocol() {
        return protocol;
    }

    /**
     * 根据给定的消息头的名字获取对应消息头的值
     * @param name
     * @return
     */
    public String getHeader(String name) {
        return headers.get(name);
    }

    public String getRequestURI() {
        return requestURI;
    }

    public String getQueryString() {
        return queryString;
    }

    public String getParameter(String name) {
        return parameters.get(name);
    }
}

V15

支持POST请求
当页面form表单中包含用户隐私信息或有附件上传时,应当使用POST形式提交。
POST会将表单数据包含在请求的消息正文中。
如果表单中没有附件,则正文中包含的表单数据就是一个字符串,而格式就是原GET形式提交时抽象路径中"?"右侧的内容。

实现:
1:完成HttpServletRequest中的解析消息正文的方法,当页面(reg.html或login.html)中form的提交方式改为POST时,表单数据被包含在正文里,并且请求的消息头中会出现Content-TypeContent-Length用于告知服务端正文内容。因此我们可以根据它们来解析正文。
2:将解析参数的操作从parseURI中单独提取出来定义在parseParameter()方法中重用。parseURI和解析正文方法parseContent都可以调用parseParameter()来重用拆分操作。

DispatcherServlet

package com.birdboot.core;

import com.birdboot.controller.UserController;
import com.birdboot.http.HttpServletRequest;
import com.birdboot.http.HttpServletResponse;

import java.io.File;
import java.net.URISyntaxException;

/**
 * V8新增内容:
 * 该类是SpringMVC框架与Tomcat整合时的一个关键类
 * Tomcat处理业务原生的都是调用继承了HttpServlet的类来完成,此时需要进行很多配置
 * 以及使用时要作很多重复性劳动。
 * SpringMVC框架提供的该类也是继承了HttpServlet的,使用它来接收处理请求的工作。
 */
public class DispatcherServlet {
    private static File baseDir;//类加载路径
    private static File staticDir;//类加载路径下的static目录

    static{
        try {
            //定位当前项目的类加载路径
            baseDir = new File(
                    DispatcherServlet.class.getClassLoader().getResource(".").toURI()
            );
            //定位类加载路径下的static目录
            staticDir = new File(baseDir, "static");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    /**
     * service方法实际上是当我们继承了HttpServlet后必须重写的方法
     * 该方法要求接收两个参数:请求对象与响应对象。
     * Tomcat在处理请求时就是调用某个Servlet的service方法并将请求与响应对象传入
     * 来让其完成处理工作的。
     */
    public void service(HttpServletRequest request, HttpServletResponse response){
        //获取请求的抽象路径
        //不能在使用uri判断请求了,因为uri可能含参数,内容不固定。
        String path = request.getRequestURI();
        System.out.println(path);

        //判断该请求是否为请求一个业务
        if("/regUser".equals(path)){
            UserController controller = new UserController();
            controller.reg(request, response);
        }else if("/loginUser".equals(path)){
            UserController controller = new UserController();
            controller.login(request, response);
        }else {
            File file = new File(staticDir, path);
            if (file.isFile()) {
                //由于响应对象中状态代码和描述默认值为200,OK因此正确情况下不用再设置
                response.setContentFile(file);
                //设置响应头
                response.addHeader("Server", "BirdServer");
            } else {
                response.setStatusCode(404);
                response.setStatusReason("NotFound");
                file = new File(staticDir, "404.html");
                response.setContentFile(file);
                response.addHeader("Server", "BirdServer");
            }
        }
    }
}

HttpServletResponse

package com.birdboot.http;

import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * V7新增内容:
 * 响应对象
 * 该类的每一个实例用于表示服务端给客户端发送的一个HTTP的响应
 * HTTP协议要求一个响应由三部分构成:状态行,响应头,响应正文
 */
public class HttpServletResponse {

    private static MimetypesFileTypeMap mftm = new MimetypesFileTypeMap();

    private Socket socket;

    //状态行相关信息
    private int statusCode = 200;//状态代码
    private String statusReason = "OK";//状态描述

    //响应头相关信息 key:响应头的名字  value:响应头的值
    private Map<String,String> headers = new HashMap<>();

    //响应正文相关信息
    private File contentFile;//响应正文对应的实体文件


    public HttpServletResponse(Socket socket){
        this.socket = socket;
    }

    /**
     * 该方法用于将当前响应对象内容以标准的HTTP响应格式发送给客户端
     */
    public void response() throws IOException {
        //3.1发送状态行
        sendStatusLine();
        //3.2发送响应头
        sendHeaders();
        //3.3发送响应正文
        sendContent();
    }

    //发送状态行
    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      42123
            Server              BirdServer
            ...                 ...
         */
        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(contentFile!=null) {
            FileInputStream fis = new FileInputStream(contentFile);
            OutputStream out = socket.getOutputStream();
            byte[] buf = new byte[1024 * 10];//10kb
            int d;//记录每次实际读取的数据量
            while ((d = fis.read(buf)) != -1) {
                out.write(buf, 0, d);
            }
        }
    }

    /**
     * V7:将ClientHandler中发送响应的工作全部移动到这里,println方法也是。
     * 向客户端发送一行字符串
     * @param line
     */
    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        out.write(data);
        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;
    }

    /**
     * 设置响应正文对应的实体文件,该方法中会自动根据该文件添加对应的两个响应头:
     * Content-Type和Content-Length
     * @param contentFile
     */
    public void setContentFile(File contentFile) {
        this.contentFile = contentFile;


        addHeader("Content-Type",mftm.getContentType(contentFile));
        addHeader("Content-Length",contentFile.length()+"");
    }

    /**
     * 添加一个响应头
     * @param name
     * @param value
     */
    public void addHeader(String name,String value){
        headers.put(name,value);
    }

    /**
     * 要求浏览器重定向到指定位置
     * @param location
     */
    public void sendRedirect(String location){
        //1设置状态代码302
        statusCode = 302;
        statusReason = "Moved Temporarily";
        //2添加响应头Location
        addHeader("Location",location);
    }
}

HttpServletRequest

package com.birdboot.http;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * V4:新增内容
 * 请求对象
 * 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
 * HTTP协议要求请求的格式由三部分构成:请求行,消息头,消息正文
 */
public class HttpServletRequest {
    private Socket socket;
    //请求行相关信息
    private String method;//请求方式
    private String uri;//抽象路径
    private String protocol;//协议版本

    private String requestURI;//保存uri中"?"左侧的请求路径部分
    private String queryString;//保存uri中"?"右侧的参数部分
    private Map<String,String> parameters = new HashMap<>();//保存每一组参数


    //消息头相关信息 key:消息头名字  value:消息头对应的值
    private Map<String,String> headers = new HashMap<>();


    public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
        this.socket = socket;

        //1.1解析请求行
        parseRequestLine();

        //1.2解析消息头
        parseHeaders();

        //1.3解析消息正文
        parseContent();
    }
    //解析请求行
    private void parseRequestLine() throws IOException, EmptyRequestException {
        String line = readLine();

        if(line.isEmpty()){//如果请求行是个空字符串,则说明本次为空请求
            throw new EmptyRequestException();
        }

        System.out.println("请求行:"+line);

        //将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
        String[] data = line.split("\\s");
        method = data[0];
        uri = data[1];
        protocol = data[2];

        parseURI();//进一步解析uri

        System.out.println("method:"+method);
        System.out.println("uri:"+uri);
        System.out.println("protocol:"+protocol);
    }
    //进一步解析uri
    private void parseURI(){
        String[] data = uri.split("\\?");
        requestURI = data[0];
        if(data.length>1){
            queryString = data[1];
            parseParameters(queryString);
        }
        System.out.println("requestURI:"+requestURI);
        System.out.println("queryString:"+queryString);
        System.out.println("parameters:"+parameters);

    }

    /**
     * 解析参数
     * 参数的格式应当是:name=value&name=value&...
     * 如果是GET请求,参数来自抽象路径的"?"右侧
     * 如果是POST请求,参数来自消息正文
     * 但是格式是一致的。
     * @param line
     */
    private void parseParameters(String line){
        String[] paraArr = line.split("&");
        for(String para : paraArr){
            String[] arr = para.split("=",2);
            parameters.put(arr[0],arr[1]);
        }
    }

    //解析消息头
    private void parseHeaders() throws IOException {
        while(true) {
            String line = readLine();
            if(line.isEmpty()){//如果读取到了空行
                break;
            }
            System.out.println("消息头:" + line);
            String[] data = line.split(":\\s");
            headers.put(data[0],data[1]);
        }
        System.out.println("headers:"+headers);
    }
    //解析消息正文
    private void parseContent() throws IOException {
        //确定本次请求是否包含正文(消息头中是否含有Content-Length)
        if(headers.containsKey("Content-Length")){
            int contentLength = Integer.parseInt(getHeader("Content-Length"));
            byte[] contentData = new byte[contentLength];
            InputStream in = socket.getInputStream();
            in.read(contentData);//读取消息正文到字节数组中

            String contentType = getHeader("Content-Type");
            //分支:判断正文类型,并进行对应的解析
            if("application/x-www-form-urlencoded".equals(contentType)){
                //正文是form表单提交的数据(原get请求提交是在抽象路径"?"右侧内容)
                String line = new String(contentData, StandardCharsets.ISO_8859_1);
                System.out.println("=============正文:"+line);
                parseParameters(line);
            }
            //后续可扩展支持其他正文类型的解析
//            else if("xxx/xxx".equals(contentType)){
//
//            }

        }
    }

    /**
     * 通过socket获取的输入流读取客户端发送过来的一行字符串
     * @return
     */
    private String readLine() throws IOException {//通常被重用的代码不自己处理异常
        //对一个socket实例调用多次getInputStream()返回的始终是同一条输入流。而输出流也是如此
        InputStream in = socket.getInputStream();
        int d;
        char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
        StringBuilder builder = new StringBuilder();//保存读取后的所有字符
        while((d = in.read())!=-1){
            cur = (char)d;//本次读取的字符
            if(pre==13 && cur==10){//是否连续读取到了回车+换行
                break;
            }
            builder.append(cur);//将本次读取的字符拼接
            pre=cur;//在进行下次读取前,将本次读取的字符保存到"上次读取的字符"中
        }
        return builder.toString().trim();
    }

    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }

    public String getProtocol() {
        return protocol;
    }

    /**
     * 根据给定的消息头的名字获取对应消息头的值
     * @param name
     * @return
     */
    public String getHeader(String name) {
        return headers.get(name);
    }

    public String getRequestURI() {
        return requestURI;
    }

    public String getQueryString() {
        return queryString;
    }

    public String getParameter(String name) {
        return parameters.get(name);
    }
}

V16

解决传递中文问题:

  • 现象:

    • 当浏览器无论是以GET形式提交还是POST形式提交表单,如果表单中含有中文信息时,所有的中文内容都会被转为:
    • “username=%E8%8C%83%E4%BC%A0%E5%A5%87&password=123456&nickname=%E4%BC%A0%E5%A5%87&age=22”,
  • 原因:

    • 以GET请求为例,表单信息会被拼接到抽象路径的"?"右侧。抽象路径是包含在浏览器发送的请求的请求行中:
    • GET /regUser?username=%E8%8C%83%E4%BC%A0%E5%A5%87&… HTTP/1.1

HTTP协议要求浏览器发送的请求的请求行和消息头必须是文本且字符集只能是ISO8859-1
实际上可传输的字符只有英文,数字,符号
因为==ISO8859-1不支持中文==,因此不能直接将中文包含在抽象路径中
GET /regUser?username=张三&… HTTP/1.1 不合法!!!

解决办法

  • 核心思想:ISO8859-1支持的字符来表达不支持的字符
  • 可以使用的字符:英文,数字,符号

例:
首先:先将中文字符"范"按照支持的字符(通常是UTF-8)转换为2进制"范"-----UTF8---->11101000 10001100 10000011
我们可以用字符"0"和字符"1"表示2进制
传递时可以将:/regUser?username=&… 不合法

换做:
/regUser?username=111010001000110010000011&… 合法

服务端接收到后,再将这一串1和0组成的内容当做2进制看待,再按照UTF-8还原就可以看到"范"

问题得到解决,但是新的问题产生了:数据太长

解决办法:将2进制用16进制表示

二进制十进制十六进制
000000
000111
001022
001133
010044
010155
011066
011177
100088
100199
101010a
101111b
110012c
110113d
111014e
111115f

原本:
/regUser?username=11101000 10001100 10000011&…
用16进制表示:
/regUser?username=E88C83&…
长度可以缩短4倍

长度问题解决了,新的问题又出现了
/regUser?username=E88C83&…
服务端接收到username后,该用户的名字是"范"还是此人就叫 E88C83
为了避免混淆,URL地址格式进行了规定
如果使用英文+数字组合表示的是16进制,则需要在每两位16进制前添加一个"%"
因此:
/regUser?username=E88C83&… 此人就叫E88C83
/regUser?username=%E8%8C%83&… 这里是16进制表示了3个字节,可转换为"范"

服务端如果想正确得到中文则需要将上述动作反推。
%E8%8C%83–>11101000 10001100 10000011—>范
该操作JAVA有现成的API:URLDecoder

HttpServletRequest

package com.birdboot.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * V4:新增内容
 * 请求对象
 * 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
 * HTTP协议要求请求的格式由三部分构成:请求行,消息头,消息正文
 */
public class HttpServletRequest {
    private Socket socket;
    //请求行相关信息
    private String method;//请求方式
    private String uri;//抽象路径
    private String protocol;//协议版本

    private String requestURI;//保存uri中"?"左侧的请求路径部分
    private String queryString;//保存uri中"?"右侧的参数部分
    private Map<String,String> parameters = new HashMap<>();//保存每一组参数

    //消息头相关信息 key:消息头名字  value:消息头对应的值
    private Map<String,String> headers = new HashMap<>();

    public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
        this.socket = socket;
        //1.1解析请求行
        parseRequestLine();
        //1.2解析消息头
        parseHeaders();
        //1.3解析消息正文
        parseContent();
    }
    //解析请求行
    private void parseRequestLine() throws IOException, EmptyRequestException {
        String line = readLine();

        if(line.isEmpty()){//如果请求行是个空字符串,则说明本次为空请求
            throw new EmptyRequestException();
        }

        System.out.println("请求行:"+line);

        //将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
        String[] data = line.split("\\s");
        method = data[0];
        uri = data[1];
        protocol = data[2];

        parseURI();//进一步解析uri

        System.out.println("method:"+method);
        System.out.println("uri:"+uri);
        System.out.println("protocol:"+protocol);
    }
    //进一步解析uri
    private void parseURI(){
        String[] data = uri.split("\\?");
        requestURI = data[0];
        if(data.length>1){
            queryString = data[1];
            parseParameters(queryString);
        }
        System.out.println("requestURI:"+requestURI);
        System.out.println("queryString:"+queryString);
        System.out.println("parameters:"+parameters);

    }

    /**
     * 解析参数
     * 参数的格式应当是:name=value&name=value&...
     * 如果是GET请求,参数来自抽象路径的"?"右侧
     * 如果是POST请求,参数来自消息正文
     * 但是格式是一致的。
     * @param line
     */
    private void parseParameters(String line){
        //将line转码
        try {
            line = URLDecoder.decode(line,"UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String[] paraArr = line.split("&");
        for(String para : paraArr){
            String[] arr = para.split("=",2);
            parameters.put(arr[0],arr[1]);
        }
    }

    //解析消息头
    private void parseHeaders() throws IOException {
        while(true) {
            String line = readLine();
            if(line.isEmpty()){//如果读取到了空行
                break;
            }
            System.out.println("消息头:" + line);
            String[] data = line.split(":\\s");
            headers.put(data[0],data[1]);
        }
        System.out.println("headers:"+headers);
    }
    //解析消息正文
    private void parseContent() throws IOException {
        //确定本次请求是否包含正文(消息头中是否含有Content-Length)
        if(headers.containsKey("Content-Length")){
            int contentLength = Integer.parseInt(getHeader("Content-Length"));
            byte[] contentData = new byte[contentLength];
            InputStream in = socket.getInputStream();
            in.read(contentData);//读取消息正文到字节数组中

            String contentType = getHeader("Content-Type");
            //分支:判断正文类型,并进行对应的解析
            if("application/x-www-form-urlencoded".equals(contentType)){
                //正文是form表单提交的数据(原get请求提交是在抽象路径"?"右侧内容)
                String line = new String(contentData, StandardCharsets.ISO_8859_1);
                System.out.println("=============正文:"+line);
                parseParameters(line);
            }
            //后续可扩展支持其他正文类型的解析
//            else if("xxx/xxx".equals(contentType)){
//            }

        }
    }

    /**
     * 通过socket获取的输入流读取客户端发送过来的一行字符串
     * @return
     */
    private String readLine() throws IOException {//通常被重用的代码不自己处理异常
        //对一个socket实例调用多次getInputStream()返回的始终是同一条输入流。而输出流也是如此
        InputStream in = socket.getInputStream();
        int d;
        char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
        StringBuilder builder = new StringBuilder();//保存读取后的所有字符
        while((d = in.read())!=-1){
            cur = (char)d;//本次读取的字符
            if(pre==13 && cur==10){//是否连续读取到了回车+换行
                break;
            }
            builder.append(cur);//将本次读取的字符拼接
            pre=cur;//在进行下次读取前,将本次读取的字符保存到"上次读取的字符"中
        }
        return builder.toString().trim();
    }

    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }

    public String getProtocol() {
        return protocol;
    }

    /**
     * 根据给定的消息头的名字获取对应消息头的值
     * @param name
     * @return
     */
    public String getHeader(String name) {
        return headers.get(name);
    }

    public String getRequestURI() {
        return requestURI;
    }

    public String getQueryString() {
        return queryString;
    }

    public String getParameter(String name) {
        return parameters.get(name);
    }
}

HttpServletResponse

package com.birdboot.http;

import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * V7新增内容:
 * 响应对象
 * 该类的每一个实例用于表示服务端给客户端发送的一个HTTP的响应
 * HTTP协议要求一个响应由三部分构成:状态行,响应头,响应正文
 */
public class HttpServletResponse {

    private static MimetypesFileTypeMap mftm = new MimetypesFileTypeMap();

    private Socket socket;

    //状态行相关信息
    private int statusCode = 200;//状态代码
    private String statusReason = "OK";//状态描述

    //响应头相关信息 key:响应头的名字  value:响应头的值
    private Map<String,String> headers = new HashMap<>();

    //响应正文相关信息
    private File contentFile;//响应正文对应的实体文件


    public HttpServletResponse(Socket socket){
        this.socket = socket;
    }

    /**
     * 该方法用于将当前响应对象内容以标准的HTTP响应格式发送给客户端
     */
    public void response() throws IOException {
        //3.1发送状态行
        sendStatusLine();
        //3.2发送响应头
        sendHeaders();
        //3.3发送响应正文
        sendContent();
    }

    //发送状态行
    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      42123
            Server              BirdServer
            ...                 ...
         */
        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(contentFile!=null) {
            FileInputStream fis = new FileInputStream(contentFile);
            OutputStream out = socket.getOutputStream();
            byte[] buf = new byte[1024 * 10];//10kb
            int d;//记录每次实际读取的数据量
            while ((d = fis.read(buf)) != -1) {
                out.write(buf, 0, d);
            }
        }
    }

    /**
     * V7:将ClientHandler中发送响应的工作全部移动到这里,println方法也是。
     * 向客户端发送一行字符串
     * @param line
     */
    private void println(String line) throws IOException {
        OutputStream out = socket.getOutputStream();
        byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
        out.write(data);
        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;
    }

    /**
     * 设置响应正文对应的实体文件,该方法中会自动根据该文件添加对应的两个响应头:
     * Content-Type和Content-Length
     * @param contentFile
     */
    public void setContentFile(File contentFile) {
        this.contentFile = contentFile;
        addHeader("Content-Type",mftm.getContentType(contentFile));
        addHeader("Content-Length",contentFile.length()+"");
    }

    /**
     * 添加一个响应头
     * @param name
     * @param value
     */
    public void addHeader(String name,String value){
        headers.put(name,value);
    }

    /**
     * 要求浏览器重定向到指定位置
     * @param location
     */
    public void sendRedirect(String location){
        //1设置状态代码302
        statusCode = 302;
        statusReason = "Moved Temporarily";
        //2添加响应头Location
        addHeader("Location",location);
    }
}

反射

JAVA反射机制

  • 反射是java的动态机制,允许程序在"运行期间"再确定如:对象的实例化,方法的调用,属性的操作等。
  • 反射机制可以提高代码的灵活性和适应性。但是会带来较多的系统开销和较慢的运行效率
  • 因此反射机制不能过度被依赖。

反射机制使用的第一步:获取待操作的类的类对象类对象:Class类的实例

  • JVM内部每个被加载的类都有且只有一个Class的实例与之对应。
  • JVM加载一个类时会读取该类的.class文件然后将其载入到JVM内部
  • 与此同时会实例化一个Class的实例,用该实例记录被加载的类的信息(类名,方法,构造器等)

获取一个类的类对象方式

  1. 类名.class
Class cls = String.class;//获取String的类对象
Class cls = int.class;//获取int的类对象(基本类型只有这一种方式获取类对象)
  1. Class.forName(String className)

    //根据类的完全限定(包名.类名)名加载并获取该类的类对象
    Class cls = Class.forName("java.lang.String");
    
  2. ClassLoader类加载器方式

反射对象

  • Class,它的每一个实例用于表示一个类的信息
  • Package,它的每一个实例用于表示一个包的信息
  • Method,它的每一个实例用于表示一个方法
  • Constructor,它的每一个实例用于表示一个构造器
  • Filed,它的每一个实例用于表示一个属性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/12599.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux网络服务】DNS域名解析服务服务

一、BIND域名服务基础 服务背景 1在日常生活中人们习惯使用域名访问服务器&#xff0c;但机器向互相只认IP地址&#xff0c;域名与IP地址之间是多对一的关系&#xff0c;一个IP址不一定只对应一个域名&#xff0c;且一个完成域名只可以对应一个IP地址&#xff0c;它们之间转换…

【数据结构】解析队列各接口功能实现

目录 前言&#xff1a; 一、队列概述&#xff1a; 1.队列的概念&#xff1a; 二、队列的各种接口功能实现&#xff1a; 1.初始化队列&#xff1a; 2.入队&#xff08;尾插&#xff09;&#xff1a; 3.出队&#xff08;头删&#xff09;&#xff1a; 4.查看队头&#xf…

PyTorch 深度学习实战 | 基于 ResNet 的花卉图片分类

“工欲善其事&#xff0c;必先利其器”。如果直接使用 Python 完成模型的构建、导出等工作&#xff0c;势必会耗费相当多的时间&#xff0c;而且大部分工作都是深度学习中共同拥有的部分&#xff0c;即重复工作。所以本案例为了快速实现效果&#xff0c;就直接使用将这些共有部…

YAML /Excel /CSV?自动化测试测试数据管理应用,测试老鸟总结...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 自动化测试无论是…

ChatGPT实战100例 - (02) 自动出PPT它不香么?

文章目录 ChatGPT实战100例 - (02) 自动出PPT它不香么&#xff1f;一、需求与思路1. 需求&#xff1a;出个PPT&#xff0c;5分钟后要用2. 思路&#xff1a;生成markdown然后转化 二、生成markdown语法的思维导图1. 问题2. 回答 三、把markdown文本转换成PPT ChatGPT实战100例 -…

MIMO-OFDM无线通信技术(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 本代码为MIMO-OFDM无线通信技术及MATLAB实现。分为十章&#xff0c;供大家学习。 &#x1f4da;2 运行结果 主函数部分代码&a…

Mysql安装详细教程

数据库相关概念 而目前主流的关系型数据库管理系统的市场占有率排名如下&#xff1a; Oracle&#xff1a;大型的收费数据库&#xff0c;Oracle公司产品&#xff0c;价格昂贵。 MySQL&#xff1a;开源免费的中小型数据库&#xff0c;后来Sun公司收购了MySQL&#xff0c;而Oracle…

逆向-还原代码之(*point)[4]和char *point[4] (Interl 64)

// source code #include <stdio.h> #include <string.h> #include <stdlib.h> /* * char (*point)[4] // 数组指针。 a[3][4] // 先申明二维数组,用它来指向这个二维数组 * char *point[4] // 指针数组。 a[4][5] // 一连串的指针…

知识点学习登记备份信息

知识点记录 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ovilnIi-1681441105895)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20211228090433836.png)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上…

全景丨0基础学习VR全景制作,平台篇第五章:开场封面功能

大家好欢迎观看蛙色平台使用教程 开场封面功能&#xff0c;现已支持开场图片和开场视频两种呈现方式&#xff0c; 分别针对PC端和移动设备访问分别设置呈现图片、视频内容&#xff0c;满足市场主流需求。 开场图片 传达主旨 1、全局-开场封面-图片 2、分别对PC端和移动设备访…

【加载plist文件展示单组数据 Objective-C语言】

一、接下来,我们要为大家演示如何通过加载plist文件,使用UITableView展示单组数据, 1.最后运行起来的效果,是一个什么效果呢,是这样一个效果: 2.这个里面,这就是一个单元格吧, 这就是一个单元格, 这个单元格里面,包括一个图片框、一个TextLabel、一个DetailLabel、…

[CVPR 2020] Regularizing Class-Wise Predictions via Self-Knowledge Distillation

Contents IntroductionClass-wise self-knowledge distillation (CS-KD)Class-wise regularizationEffects of class-wise regularization ExperimentsClassification accuracy References Introduction 为了缓解模型过拟合&#xff0c;作者提出 Class-wise self-knowledge di…

3.rabbitmq-集群

1.修改3台的主机名称,也可以不改 vi /etc/hostname 2.配置各个节点的host文件,让各节点都能识别对方 vi /etc/hosts 192.168.3.132 host-rabbitmq 192.168.3.133 host-rabbitmq2 192.168.3.134 host-rabbitmq3 3.以确保各个节点的cookie文件使用的同一个值 在node1上执行远程命…

PostgreSQL技术内幕(七)索引扫描

索引概述 数据库索引&#xff0c;是将一个表的某些字段的数据进行重新组织的数据库对象。通过使用索引&#xff0c;可以大大加速数据库的一些操作&#xff0c;其背后的思想也很简单朴素&#xff1a;空间换时间。 数据库中的索引&#xff0c;可以类比为一本书的目录&#xff0…

远程代码执行渗透与防御

远程代码执行渗透与防御 1.简介2.PHP RCE常见函数3.靶场练习4.防御姿势 1.简介 远程代码执行漏洞又叫命令注入漏洞 命令注入是一种攻击&#xff0c;其目标是通过易受攻击的应用程序在主机操作系统上执行任意命令。 当应用程序将不安全的用户提供的数据&#xff08;表单、cook…

如何在Java中创建临时文件?

在Java程序中&#xff0c;有时需要创建临时文件来暂存数据或者执行某些操作。Java提供了许多方式来创建临时文件。在本教程中&#xff0c;我们将介绍如何使用Java标准库来创建临时文件。 一、使用File.createTempFile()方法 Java标准库中的File类提供了createTempFile()方法来…

条款08: 别让异常逃离析构函数

文章目录 背景知识析构函数 背景知识 下面是一段测试代码&#xff1a; class Test { public:Test(int para){m_num para;};void test_throw(){throw(3);};~Test() {cout<<"delete Test"<<m_num<<endl;//test_throw();};int m_num; }; int main(…

无线耳机哪个音质比较好?四百内音质最好的无线耳机排行

蓝牙耳机常常作为手机的伴生产品而出现在人们的日常生活当中&#xff0c;其使用场景也越来越广泛。而随着蓝牙技术的发展&#xff0c;蓝牙耳机在音质上的表现也越来越好。下面&#xff0c;我来给大家推荐几款四百内音质最好的无线耳机&#xff0c;一起来看看吧。 一、南卡小音舱…

Java多线程基础面试总结(三)

线程的生命周期和状态 Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态&#xff1a; NEW&#xff1a;初始状态&#xff0c;线程被创建出来&#xff0c;但是还没有调用start()方法。RUNABLE&#xff1a;运行中状态&#xff0c;调用了start()…

什么是 AUTOSAR C++14?

总目录链接>> AutoSAR入门和实战系列总目录 总目录链接>> AutoSAR BSW高阶配置系列总目录 文章目录 什么是 AUTOSAR C14&#xff1f;AUTOSAR C14 规则和偏差静态分析工具可以完全支持自动 什么是 AUTOSAR C14&#xff1f; 它是 C 版本 14 (ISO/IEC 14882:2014…
最新文章