Реализуйте Tomcat вместе со мной (2): реализация простого контейнера сервлетов

Java Java EE Tomcat
Реализуйте Tomcat вместе со мной (2): реализация простого контейнера сервлетов

предисловие

章节循序渐进的讲解了tomcat的原理,在接下来的章节中,tomcat都是基于上一章新增功能并完善,
到最后形成一个简易版tomcat的完成品。所以有兴趣的同学请按顺序阅读,本文为记录第二章的知识点
以及源码实现(造轮子)。

Обзор контента

Внедряйте Tomcat вместе со мной (1): Внедрение статического веб-сервера

В предыдущей главе мы реализовали простой веб-сервер статических ресурсов, который может считывать определяемые пользователем HTML/css/js/изображения и отображать их в браузере и отображать страницы 404.

Содержание этой главы

В этой главе будет реализован простой контейнер сервлетов, который может вызывать и выполнять метод service() соответствующего сервлета в соответствии с URI пользовательского запроса.Методы init()/destory() и большинство методов в HttpServletRequest/HttpServletResponse еще не реализованы в этой главе., который будет постепенно улучшаться в следующих главах.

до начала

  • javax.servlet.Servlet

    咱们web开发的同学都知道,刚学习web开发的时候都是先实现这个Servlet接口去自定义自己的
    Servlet类的,那么在这里简单的回顾一下Servlet这个接口。

    Добавьте зависимость в проект:

    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.0.1</version>
    </dependency>

    Список методов интерфейса сервлета (каждый должен знать, что делает конкретный метод, поэтому я не буду его представлять):

    public interface Servlet {
    public void init(ServletConfig config) throws ServletException;
    public ServletConfig getServletConfig();
    public void service(ServletRequest req, ServletResponse res)throws ServletException, IOException;
    public String getServletInfo();public void destroy();
    }
  • Как добиться

    Основываясь на коде из предыдущей главы, пока пользователь вводит 127.0.0.1:8080/servlet/{servletName}, мы будем извлекать конкретное имя сервлета из этого URI и использовать URLClassLoader в пакете java.net для загрузки класс сервлета и Instantiate, а затем вызвать его метод service(), вызов сервлета делается так, не правда ли, это очень просто, давайте посмотрим, как реализован код! ! !

Код

1. Реализовать соответствующий интерфейс

Давайте сначала реализуем интерфейсы ServletRequest и ServletResponse (это спецификация сервлета) для Request и Response в предыдущей главе Мы ничего не делаем для конкретного метода реализации, и мы улучшим его позже.

public class Request implements ServletRequest {
...省略N个方法
}
public class Response implements ServletResponse {
/*Response只实现这个方法,把我们socket的outputStream封装成一个PrintWriter*/
@Override
public PrintWriter getWriter() throws IOException {
PrintWriter writer = new PrintWriter(outputStream,true);
return writer;
}
}

2. Разные ресурсы используют разных исполнителей

Наш tomcat готов поддерживать вызовы сервлетов, поэтому сервлеты отличаются от обычных статических ресурсов, поэтому мы должны изолировать их на уровне кода, чтобы облегчить будущее расширение.Здесь мы реализуем следующие два исполнителя:

 - ServletProcess 专门执行Servlet的执行器
- StaticResourceProcess 执行静态资源的执行器

Затем мы посмотрим на процесс реализации, который мы сейчас являемся запросом:


Ну на самом деле, как видите, изменение не очень большое по сравнению с предыдущим, но есть дополнительное if суждение, а затем в исполняющем кидается соответствующий исполнительный процесс на исполнение. выполнение:

  • HttpServer

    Вы должны еще помнить HttpServer, который является главным входом в нашу программу запуска и реализацию мониторинга ServerSocket.
    Это не сильно меняет, просто добавляет суждение if:

public static void main(String[] args) {
ServerSocket serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
....
//解析用户的请求
Request request = new Request();
request.setRequestStream(inputStream);
request.parseRequest();
//生成相应的响应
Response response = new Response(outputStream, request);
//根据URI调用不同的处理器处理请求
if (request.getUri().startsWith("/servlet/")) {
new ServletProcess().process(request, response);
} else {
new StaticResourceProcess().process(request, response);
}
...
}
  • StaticResourceProcess

    StaticResourceProcess ничего не делал, просто вызывал метод чтения статических ресурсов из предыдущей главы

public class StaticResourceProcess {
public void process(Request request, Response response) throws IOException {
response.accessStaticResources();
}
}
  • ServletProcess

    ServletProcess содержит статическую переменную URLClassLoader, которая специально используется для загрузки сервлетов:

    private static final URLClassLoader URL_CLASS_LOADER;
    static {
    /*定位到我们的webroot/servlet/文件夹*/
    URL servletClassPath = new File(HttpServer.WEB_ROOT, "servlet").toURI().toURL();
    //初始化classloader
    URL_CLASS_LOADER = new URLClassLoader(new URL[]{servletClassPath});
    }

    Теперь мы знаем, что запросы URI, начинающиеся с /servlet/, должны вызывать ресурсы сервлета, так как же нам извлечь имя сервлета и инициализировать его? Давайте сначала посмотрим на URI:

    /servlet/TestServlet

    Извлечь не сложно, вы можете сделать это напрямую с помощью методов String lastIndexOf и substring:

    uri = uri.substring(uri.lastIndexOf("/") + 1);

    Предыдущие проблемы были решены, поэтому давайте посмотрим, как выполняется процесс:

public void process(Request request, Response response) throws IOException {
//就是上面的那个字符串截取方法
String servletName = this.parseServletName(request.getUri());
//使用URLClassLoader加载这个Servlet并实例化
Class servletClass = = URL_CLASS_LOADER.loadClass(servletName);
Servlet servlet = (Servlet) servletClass.newInstance();
response.getWriter().println(new String(response.responseToByte(HttpStatusEnum.OK)));
//调用servlet的service方法
servlet.service(request,response);
}

Вы можете не понять код в предпоследней строке, он вызывает объект Response.PrintWriter (который мы только что инкапсулировали с помощью outputStream сокета) и выводит заголовок ответа в браузер (хром, который не делает этого нагло, подумает этот ответ недействителен, и содержимое, отраженное сервлетом, не может быть просмотрено

)
ServletProcess грубо вызывает процесс:

3. Подготовьте собственный сервлет

Наш контейнер сервлетов разработан, давайте сделаем сервлет для эксперимента~

public class TestServlet implements Servlet {
public void init(ServletConfig config) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
System.out.println("Start invoke TestServlet ... ");
res.getWriter().println("Hello Servlet!");
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
}

Он просто выводит запись на консоль и выводит предложение в браузер (Это чувство, что очень скучно, что он не может обрабатывать параметры. В следующих главах мы будем реализовать его.), скомпилируйте этот класс в файл класса, поместите его в нашу папку resource/webroot/servlet, откройте браузер и пройдитесь:

Возьми!

Нет... На самом деле у вышеописанной конструкции есть серьезные недостатки

Повышение безопасности запроса и ответа

  • Где дефект

Заботливые друзья, должно быть, узнали: когда мы вызываем определяемый пользователем сервлет в ServletProcess,Он заключается в прямой передаче запроса/ответа в качестве параметра в метод службы пользователя.(Поскольку наши reuqest и response реализуют интерфейсы ServletRequest и ServletResponse), то, если наш tomcat будет передан другим людям для использования, сервлет тех, кто прочитал исходный код нашего tomcat, может написать так:

public class TestServlet {
public void service(HttpServletRequest request,HttpServletResponse response){
((Request)request).parseRequest("");
((Response)response).accessStaticResources();
}
}

Вышеупомянутые два метода предназначены для обеспечения нашего процесса или использования в другое время (поэтому метод не может быть установлен как частный),не предоставляется пользователю для вызова, что нарушает инкапсуляцию! !

  • решение

    Когда вы видели или читали исходный код Tomcat, вы обнаружили, что Tomcat использовал шаблон проектирования для устранения этого дефекта, то естьШаблон проектирования экстерьера (шаблон проектирования фасада), вы можете выполнить поиск определенного шаблона проектирования. Здесь мы также ссылаемся на этот шаблон проектирования, чтобы справиться с этим дефектом. Отношение диаграммы классов UML выглядит следующим образом:


Код также очень прост для вызова соответствующего метода внутреннего объекта запроса:

public class RequestFacade implements ServletRequest{
private Request request;
@Override
public Object getAttribute(String name) {
return request.getAttribute(name);
}
其他实现的方法也类似...
}

Когда метод ServletProcess вызывает сервлет, мы оборачиваем его классом Facade:

...
Servlet servlet = (Servlet) servletClass.newInstance();
servlet.service(new RequestFacade(request), new ResponseFacade(response));
...

Вот и все!

使用者顶多只能将ServletRequest/ServletResponse向下转型为RequestFacade/ResponseFacade 
但是我们没提供getReuqest()/getResponse()方法,所以它能调用的方法还是相应ServletRequest、
ServletResponse接口定义的方法,这样我们内部的方法就不会被用户调用到啦~

На данный момент наш веб-сервер Tomcat 2.0 уже разработан (забавное лицо), и можно реализовать простой пользовательский вызов сервлета, но многие функции все еще не идеальны:

 - 每一次请求就new一次Servlet,Servlet应该在初始化项目时就应该初始化,是单例的。
- 并未遵循Servlet规范实现相应的生命周期,例如init()/destory()方法我们均未调用。
- ServletRequest/ServletResponse接口的方法我们仍未实现
- 其他未实现的功能

В следующей главе мы реализуем запрос для анализа параметров, HTTPHeader, Cookie и других параметров и реконструируем архитектурный шаблон:

Реализовать Tomcat вместе со мной (3): Разобрать параметры запроса запроса, заголовки запроса, файлы cookie

PS: Исходный код этой главы выложен на github. SimpleTomcat