ZetCode

嵌入式 Jetty

最后修改于 2024 年 1 月 27 日

Jetty 可以以嵌入式模式运行。这意味着不需要构建 WAR 文件并将其部署到独立的 Jetty 服务器。Jetty 是一个软件组件,可以像任何其他 POJO(Plain Old Java Object)一样被实例化和使用。

简单处理器

Handler 是一个处理传入请求的组件。请求在 handle 方法中处理。Handlers 会接收 servlet API 的请求和响应对象,但它们本身不是 servlets。

$ mkdir simplehandler
$ cd simplehandler/
$ mkdir -p src/com/zetcode
$ mkdir build
$ touch src/com/zetcode/SimpleHandlerEx.java

我们创建一个新的项目目录及其结构。

SimpleHandlerEx.java
package com.zetcode;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
 
public class SimpleHandlerEx extends AbstractHandler {
 
    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request, 
        HttpServletResponse response) throws IOException, ServletException {
        
        response.setContentType("text/plain;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        baseRequest.setHandled(true);
        response.getWriter().println("Hello there");
    }
 
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);
        server.setHandler(new SimpleHandlerEx());
        server.start();
        server.join();
    }
}

服务器启动,并为传入请求设置了处理器。

response.setContentType("text/plain;charset=utf-8");

由于我们只输出纯文本,因此我们将媒体类型设置为 text/plain。Internet 媒体类型是 Internet 上用于指示文件包含的数据类型的标准标识符。

response.getWriter().println("Hello there");

作为响应,我们发送了一个简单的消息。getWriter 方法返回一个 PrintWriter 对象,该对象将字符文本发送到客户端。

server.join();

join 方法使服务器线程与当前线程合并。它是阻塞的,直到服务器准备就绪。该方法还会等待直到服务器完全停止。

build.xml
<?xml version="1.0" encoding="UTF-8"?>

<project name="SimpleHandlerEx" default="compile">
    
    <property name="name" value="simplehandler"/>
    <property environment="env"/>
    <property name="src.dir" value="src"/>
    <property name="build.dir" value="build"/>
    <property name="jetty.lib.dir" location="${env.JETTY_HOME}/lib"/>
    
    <path id="compile.classpath">
        <fileset dir="${jetty.lib.dir}"> 
            <include name="**/*.jar"/> 
        </fileset>     
    </path>
        
    <path id="run.classpath">
        <pathelement path="${build.dir}"/>
        <fileset dir="${jetty.lib.dir}"> 
            <include name="**/*.jar"/> 
        </fileset>
    </path>   
        
    <target name="init">
        <mkdir dir="${build.dir}"/>
    </target>     
    
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${build.dir}" 
                  includeantruntime="false">
            <classpath refid="compile.classpath"/>
        </javac>
        <echo>Compilation completed</echo>
    </target>
    
    <target name="run" depends="compile">
        <echo>Running the program</echo>
        <java classname="com.zetcode.SimpleHandlerEx" 
                 classpathref="run.classpath"/>
    </target>         
    
    <target name="clean" depends="init">
        <delete dir="${build.dir}"/>
        <echo>Cleaning completed</echo>
    </target>  
    
</project>

构建文件包含一个 run 任务,该任务执行编译后的应用程序。

<property name="jetty.lib.dir" location="${env.JETTY_HOME}/lib"/>

为了编译和运行我们的示例,我们需要一些 JAR 文件。它们位于 Jetty 主目录的 lib/ 子目录中。

$ ant run
$ curl localhost:8080
Hello there

当我们使用 curl 应用程序发送一个简单的 GET 请求时,我们会收到此消息。

定义上下文根

Web 应用程序的上下文根决定了哪些 URL 将被委派给该应用程序。Jetty 有 ContextHandler 来定义应用程序的上下文。

ContextEx.java
package com.zetcode;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;

class MyHandler extends AbstractHandler {

    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request,
            HttpServletResponse response) throws IOException, ServletException {

        response.setContentType("text/plain;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        baseRequest.setHandled(true);
        response.getWriter().println("Hello there");
    }
}

public class ContextEx {

    public static void main(String[] args) throws Exception {

        Server server = new Server(8080);

        ContextHandler con = new ContextHandler();
        con.setContextPath("/path");
        con.setHandler(new MyHandler());
        
        server.setHandler(con);

        server.start();
        server.join();
    }
}

我们的应用程序的处理器将为名为 /path 的上下文根调用。

ContextHandler context = new ContextHandler();
context.setContextPath("/path");

实例化一个 ContextHandler 并设置路径。

context.setHandler(new MyHandler());

将处理器设置到上下文处理对象上。

server.setHandler(context);

我们使用 setHandler 方法将上下文处理对象设置到服务器上。

$ curl localhost:8080/path/
Hello there

我们将请求发送到定义的上下文根。对于不匹配的上下文,Jetty 返回 404 错误消息。

简单的 Servlet

在下面的示例中,当向嵌入式 Jetty 发送带有特定 URL 的请求时,将启动一个 Java servlet。

$ mkdir simpleservlet
$ cd simpleservlet/
$ mkdir -p src/com/zetcode/
$ touch src/com/zetcode/SimpleServlet.java
$ touch src/com/zetcode/SimpleApp.java
$ touch src/web/WEB-INF/web.xml
$ touch build.xml

我们创建了一个项目目录及其结构。我们使用 Ant 来构建项目,并使用 web.xml 部署描述符来配置我们的 Web 应用程序。

$ tree
.
├── build.xml
└── src
    ├── com
    │   └── zetcode
    │       ├── SimpleApp.java
    │       └── SimpleServlet.java
    └── web
        └── WEB-INF
            └── web.xml

5 directories, 4 files

此时,这是项目目录的内容。

SimpleApp.java
package com.zetcode;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

public class SimpleApp {

    public static void main(String[] args) throws Exception {

        String webdir = "src/web/";

        Server server = new Server(8080);
        WebAppContext wcon = new WebAppContext();

        wcon.setContextPath("/simserv");
        wcon.setDescriptor(webdir + "/WEB-INF/web.xml");
        wcon.setResourceBase(webdir);
        
        wcon.setParentLoaderPriority(true);

        server.setHandler(wcon);

        server.start();
        server.join();
    }
}

为了在嵌入式模式下配置我们的 Web 应用程序,我们使用了 WebAppContext 类。此类是 ContextHandler 的扩展,它协调 Web 应用程序的处理器的构建和配置。

wcon.setContextPath("/simserv");

使用 setContextPath 方法定义上下文路径。

wcon.setDescriptor(webdir + "/WEB-INF/web.xml");

使用 setDescriptor 方法设置部署描述符。

wcon.setResourceBase(webdir);

我们为应用程序设置文档根。它是包含上下文的静态资源的目录(或目录集合或 URL)。这些可以是图像、HTML 文件或 JSP 文件。

SimpleServlet.java
package com.zetcode;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;

public class SimpleServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        response.setContentType("text/plain");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().println("Simple servlet");
    }
}

SimpleServlet 将以纯文本消息进行响应。

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
             http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
         
  <servlet>
    <servlet-name>SimpleServlet</servlet-name>
    <servlet-class>com.zetcode.SimpleServlet</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>SimpleServlet</servlet-name>
    <url-pattern>simple.do</url-pattern>
  </servlet-mapping>
  
</web-app>

web.xml 文件中,com.zetcode.SimpleServlet 的执行被映射到字符串 simple.do。这必须在请求 URL 的末尾指定。

build.xml
<?xml version="1.0" encoding="UTF-8"?>

<project name="SimpleServlet" default="compile">
  
    <property name="name" value="simple"/>
    <property environment="env"/>
    <property name="src.dir" value="src"/>
    <property name="web.dir" value="${src.dir}/web"/>
    <property name="build.dir" location="${web.dir}/WEB-INF/classes"/>
    <property name="jetty.lib.dir" location="${env.JETTY_HOME}/lib"/>
  
    <path id="compile.classpath">
        <fileset dir="${jetty.lib.dir}"> 
            <include name="**/*.jar"/> 
        </fileset>     
    </path>
        
    <path id="run.classpath">
        <pathelement path="${build.dir}" />
        <fileset dir="${jetty.lib.dir}"> 
            <include name="**/*.jar"/> 
        </fileset>
    </path>  
  
    <target name="init">
        <mkdir dir="${build.dir}"/>
    </target>     
  
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${build.dir}" 
               includeantruntime="false">
            <classpath refid="compile.classpath"/>
        </javac>
        <echo>Compilation completed</echo>
    </target>
  
    <target name="clean" depends="init">
        <delete dir="${build.dir}"/>
        <echo>Cleaning completed</echo>
    </target>  
    
    <target name="run" depends="compile">
        <echo>Running the program</echo>
        <java classname="com.zetcode.SimpleApp" 
              classpathref="run.classpath"/>
    </target>      
    
</project>

这是配套的 Ant 构建文件。

$ ant run
$ curl localhost:8080/simserv/simple.do
Simple servlet

我们构建并运行应用程序。向 localhost:8080/simserv/simple.do 发送 GET 请求会返回“Simple servlet”纯文本消息。

@WebServlet 注解

@WebServlet 注解用于声明一个 servlet。该注解由 servlet 容器在部署时处理。声明的 servlet 可在指定的 URL 模式下访问。

$ tree
.
├── build.xml
└── src
    ├── com
    │   └── zetcode
    │       ├── AnnotatedAppEx.java
    │       └── MyServlet.java
    └── web
        └── WEB-INF

5 directories, 3 files

这是我们项目目录的内容。在此示例中,我们不包含 web.xml 文件,因为使用 Servlet 3.1 API,此文件的包含是可选的。

AnnotatedAppEx.java
package com.zetcode;

import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.plus.webapp.EnvConfiguration;
import org.eclipse.jetty.plus.webapp.PlusConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.FragmentConfiguration;
import org.eclipse.jetty.webapp.MetaInfConfiguration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
import org.eclipse.jetty.webapp.WebXmlConfiguration;

public class AnnotatedAppEx {

    public static void main(String[] args) throws Exception {
    
        String webdir = "src/web/";

        Server server = new Server(8080);
        WebAppContext wcon = new WebAppContext();
        wcon.setResourceBase(webdir);

        wcon.setContextPath("/annotated");

        wcon.setConfigurations(new Configuration[] {
            new AnnotationConfiguration(), new WebXmlConfiguration(),
            new WebInfConfiguration(), new PlusConfiguration(), 
            new MetaInfConfiguration(), new FragmentConfiguration(), 
            new EnvConfiguration() });    

        wcon.setParentLoaderPriority(true);

        server.setHandler(wcon);
        server.start();
        server.join();
    }
}

此类启动了一个启用了注解的嵌入式 Jetty 服务器。

wcon.setConfigurations(new Configuration[] {
    new AnnotationConfiguration(), new WebXmlConfiguration(),
    new WebInfConfiguration(), new PlusConfiguration(), 
    new MetaInfConfiguration(), new FragmentConfiguration(), 
    new EnvConfiguration() });  

为了启用注解,我们需要设置这些配置类。这提供了对多个功能的支持。

MyServlet.java
package com.zetcode;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet(urlPatterns = { "/aserv" })
public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        response.setContentType("text/plain");
        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().println("MyServlet called");
    }
}

MyServlet.java 文件中,我们提供了 @WebServlet 注解。该 servlet 可在 /aserv URL 下访问。

build.xml
<?xml version="1.0" encoding="UTF-8"?>

<project name="AnnotatedServlet" default="compile">
  
    <property name="name" value="annotated"/>
    <property environment="env"/>
    <property name="src.dir" value="src"/>
    <property name="web.dir" value="${src.dir}/web"/>
    <property name="build.dir" location="${web.dir}/WEB-INF/classes"/>
    <property name="jetty.lib.dir" location="${env.JETTY_HOME}/lib"/>
  
    <path id="compile.classpath">
        <fileset dir="${jetty.lib.dir}"> 
            <include name="**/*.jar"/> 
        </fileset>     
    </path>
        
    <path id="run.classpath">
        <pathelement path="${build.dir}"/>
        <fileset dir="${jetty.lib.dir}"> 
            <include name="**/*.jar"/> 
        </fileset>
    </path>         
  
    <target name="init">
        <mkdir dir="${build.dir}"/>
    </target>     
  
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${build.dir}" 
               includeantruntime="false">
            <classpath refid="compile.classpath"/>
        </javac>
        <echo>Compilation completed</echo>
    </target>
  
    <target name="clean" depends="init">
        <delete dir="${build.dir}"/>
        <echo>Cleaning completed</echo>
    </target>  
    
    <target name="run" depends="compile">
        <echo>Running the program</echo>
        <java classname="com.zetcode.AnnotatedAppEx" 
              classpathref="run.classpath"/>
    </target>         
    
</project>

这是我们项目的 Ant 构建文件。

$ ant run
$ curl localhost:8080/annotated/aserv
MyServlet called

MyServlet 已成功启动。

启用 JavaServer Pages

为了在嵌入式 Jetty 模式下启用 JavaServer Pages,我们需要创建一个临时 servlet 目录,设置一个非系统类加载器,添加一个 jsp servlet,以及添加一个 default servlet。

在下面的示例中,我们创建一个简单的 JavaServer Page 并构建一个 WAR 存档。此存档将被设置到以嵌入式模式启动的 Jetty。

$ pwd
/home/janbodnar/prog/jetty/jspexample
$ tree
.
├── build.xml
└── src
    ├── com
    │   └── zetcode
    │       └── JSPExample.java
    └── web
        ├── index.jsp
        └── WEB-INF

5 directories, 3 files

这是项目结构。

index.jsp
<!DOCTYPE html>
<html> 
<body>
<p>
   Today's date: <%= (new java.util.Date()).toLocaleString() %>
</p>
</body> 
</html>

我们的 JSP 将输出当前本地化的日期。

package com.zetcode;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import org.apache.jasper.servlet.JspServlet;
import org.apache.tomcat.InstanceManager;
import org.apache.tomcat.SimpleInstanceManager;
import org.eclipse.jetty.annotations.ServletContainerInitializersStarter;
import org.eclipse.jetty.apache.jsp.JettyJasperInitializer;
import org.eclipse.jetty.plus.annotation.ContainerInitializer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.webapp.WebAppContext;

public class JSPExample {
    
    private void startServer() throws Exception {
        
        String jetty_base = "/home/janbodnar/prog/jetty/my-base";

        File tmpdir = new File(System.getProperty("java.io.tmpdir"));
        File scdir = new File(tmpdir.toString(), "embedded-jetty-jsp");

        if (!scdir.exists()) {
            if (!scdir.mkdirs()) {
                throw new IOException("Unable to create scratch directory: " + scdir);
            }
        }        

        Server server = new Server(8080);

        WebAppContext wcon = new WebAppContext();
        wcon.setParentLoaderPriority(true);
        wcon.setContextPath("/");
        wcon.setAttribute("javax.servlet.wcon.tempdir", scdir);
        wcon.setAttribute(InstanceManager.class.getName(), 
            new SimpleInstanceManager());
        server.setHandler(wcon);
        
        JettyJasperInitializer sci = new JettyJasperInitializer();
        ServletContainerInitializersStarter sciStarter = 
            new ServletContainerInitializersStarter(wcon);
        ContainerInitializer initializer = new ContainerInitializer(sci, null);
        List<ContainerInitializer> initializers = new ArrayList<>();
        initializers.add(initializer);

        wcon.setAttribute("org.eclipse.jetty.containerInitializers", initializers);
        wcon.addBean(sciStarter, true);
        
        ClassLoader jspClassLoader = new URLClassLoader(new URL[0], 
            this.getClass().getClassLoader());
        wcon.setClassLoader(jspClassLoader);
        
        ServletHolder holderJsp = new ServletHolder("jsp", JspServlet.class);
        holderJsp.setInitOrder(0);
        holderJsp.setInitParameter("fork","false");
        holderJsp.setInitParameter("keepgenerated", "true");
        wcon.addServlet(holderJsp, "*.jsp");
        
        ServletHolder holderDefault = new ServletHolder("default", 
            DefaultServlet.class);
        holderDefault.setInitParameter("dirAllowed", "true");
        wcon.addServlet(holderDefault, "/");        
        
        wcon.setWar(jetty_base + "/webapps/jspexample.war");
        server.setHandler(wcon);

        server.start();
        server.join();        
    }

    public static void main(String[] args) throws Exception {

        JSPExample ex = new JSPExample();
        ex.startServer();
    }
}

我们启用 JSP 支持,并将一个名为 jspexample.war 的 WAR 文件设置到 Web 应用程序上下文中。

File tmpdir = new File(System.getProperty("java.io.tmpdir"));
File scdir = new File(tmpdir.toString(), "embedded-jetty-jsp");

if (!scdir.exists()) {
    if (!scdir.mkdirs()) {
        throw new IOException("Unable to create scratch directory: " + scdir);
    }
}        

我们为 servlet 上下文创建了一个临时目录。它在 JSP 编译期间使用。

System.setProperty("org.apache.jasper.compiler.disablejsr199","false");

这一行强制使用标准 javac。(也有 Eclipse 使用的。)

JettyJasperInitializer sci = new JettyJasperInitializer();
ServletContainerInitializersStarter sciStarter = 
    new ServletContainerInitializersStarter(wcon);
ContainerInitializer initializer = new ContainerInitializer(sci, null);
List<ContainerInitializer> initializers = new ArrayList<>();
initializers.add(initializer);

wcon.setAttribute("org.eclipse.jetty.containerInitializers", initializers);
wcon.addBean(sciStarter, true);

这是 JSP 初始化代码。

ClassLoader jspClassLoader = new URLClassLoader(new URL[0], 
    this.getClass().getClassLoader());
wcon.setClassLoader(jspClassLoader);

JSP 需要一个非系统类加载器,这只是包装了嵌入式系统类加载器,使其适合 JSP 使用。

ServletHolder holderJsp = new ServletHolder("jsp", JspServlet.class);
holderJsp.setInitOrder(0);
holderJsp.setInitParameter("fork","false");
holderJsp.setInitParameter("keepgenerated", "true");
wcon.addServlet(holderJsp, "*.jsp");

我们添加了一个 JspServlet。它的名称必须是“jsp”。

ServletHolder holderDefault = new ServletHolder("default", 
    DefaultServlet.class);
holderDefault.setInitParameter("dirAllowed", "true");
wcon.addServlet(holderDefault, "/");   

我们添加了一个 DefaultServlet。它的名称必须是“default”。

wcon.setWar(jetty_base + "/webapps/jspexample.war");

WAR 文件被设置到 Web 应用程序上下文中。

build.xml
<?xml version="1.0" encoding="UTF-8"?>

<project name="JSPExample" default="compile">
  
    <property name="name" value="jspexample"/>
    <property environment="env"/>
    <property name="src.dir" value="src"/>
    <property name="web.dir" value="${src.dir}/web"/>
    <property name="build.dir" location="${web.dir}/WEB-INF/classes"/>
    <property name="dist.dir" location="dist"/>
    <property name="jetty.lib.dir" location="${env.JETTY_HOME}/lib"/>
    <property name="jetty.base" location="${env.JETTY_BASE}"/>
    <property name="deploy.path" location="${jetty.base}/webapps"/>
    
    <path id="compile.classpath">
        <fileset dir="${jetty.lib.dir}"> 
            <include name="**/*.jar"/> 
        </fileset>     
    </path>
        
    <path id="run.classpath">
        <pathelement path="${build.dir}"/>
        <fileset dir="${jetty.lib.dir}"> 
            <include name="**/*.jar"/> 
        </fileset>
    </path>      
  
    <target name="init">
        <mkdir dir="${build.dir}"/>
        <mkdir dir="${dist.dir}"/>
    </target>     
  
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}" destdir="${build.dir}" 
               includeantruntime="false">
            <classpath refid="compile.classpath"/>
        </javac>
        <echo>Compilation completed</echo>
    </target>
    
    <target name="archive" depends="compile">
        <war destfile="${dist.dir}/${name}.war" needxmlfile="false">
            <fileset dir="${web.dir}"/>
        </war>
        <echo>Archive created</echo>
    </target>     
    
    <target name="clean" depends="init">
        <delete dir="${build.dir}"/>
        <delete dir="${dist.dir}"/>
        <echo>Cleaning completed</echo>
    </target>  
    
    <target name="deploy" depends="archive">
        <copy file="${dist.dir}/${name}.war" todir="${deploy.path}"/>
        <echo>Archive deployed</echo>
    </target>        
    
    <target name="run" depends="deploy">
        <echo>Running the program</echo>
        <java classname="com.zetcode.JSPExample" 
              classpathref="run.classpath"/>
    </target>     
    
</project>

这是我们项目的 Ant 构建。所有必需的 jar 文件都在 JETTY_HOME 目录的 lib 子目录中。

执行 ant run 会导致以下安全异常:java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "getClassLoader")。

我们需要向 java.policy 文件添加一个配置选项。

$ vi $JAVA_HOME/jre/lib/security/java.policy

我们编辑 java.policy 文件。

permission java.lang.RuntimePermission "getClassLoader", "read";

我们添加此权限。

$ ant run
$ curl localhost:8080/index.jsp
<!DOCTYPE html>
<html> 
<body>
<p>
    Today's date: Sep 16, 2014 1:54:05 PM
</p>
</body> 
</html>

JSP 返回当前日期。

在 Jetty 教程的这一部分,我们处理了嵌入式模式下的 Jetty。我们定义了一个简单的处理器和 servlet,使用了 @WebServlet 注解,并启用了 JSP 支持。

作者

我的名字是 Jan Bodnar,我是一名充满激情的程序员,拥有丰富的编程经验。自 2007 年以来,我一直在撰写编程文章。到目前为止,我已撰写了 1,400 多篇文章和 8 本电子书。我在教授编程方面拥有十多年的经验。

列出所有Java教程