Files
OneMD/posts/blog/编程技术/java/JavaWeb/Java Web 开发基础.md
T
2026-06-19 14:45:07 +08:00

27 KiB
Raw Blame History

title, date, tags
title date tags
Java Web 开发基础 2025-01-26
JavaWeb

Java Web 开发基础

  • C/S架构(Client/Server

    • 优点:
      • 高性能、用户体验好
    • 缺点:
      • 需要安装且不方便更新
      • 不方便维护
    • 二层:客户端、服务器
  • B/S架构(Browser/Server

    • 优点:
      • 不需要安装、使用方便
      • 更新方便
    • 缺点:
      • 性能有一点限制
    • 三层
      • 表现层
      • 业务逻辑层
      • 数据访问层
  • Web 开发相关知识

    • 静态web资源:固定的文件,如视频、图片、文件等

      • 只需要web服务器处理

      ![image28.png](Java Web 开发基础/image28.png)

    • 动态web资源:根据用户要求动态生成的内容,所以依赖服务器的处理

      • 需要处理需求的代码
      • 返回json资源、生成的网页(jsp)

    ![image](Java Web 开发基础/image114.png)

    这种开发方式是因为之前的javaScript的发展不成熟,浏览器自身性能不足、对js的支持不统一、异步请求不成熟,所以动态页面全都靠生成响应

    [!important] Web应用开发好后,若想供外界访问,需要把web应用所在目录交给web服务器管理,这个过程称之为虚似目录的映射

    ![1_Java_Web开发基础.pptx](Java Web 开发基础/1_Java_Web开发基础.pptx)

![image28.png](Java Web 开发基础/image28.png)

了解Tomcat

  • 文件位置
    • bin中有启动程序
    • config中有配置文件
  • 项目部署
    • 将项目打包成war包,放在webapps中,并启动即可
    • 静态文件有默认的servlet进行响应
  • 配置信息
    • 乱码配置
    • 管理配置

Servlet 编程

  • 基本作用

    • 处理网络请求类
    • 接收请求
    • 响应内容
  • 生命周期:每个请求对应一个线程

    • <mar每个请求对应一个线程

      ![image 2 7.png](Java Web 开发基础/image27.png)

    • 加载并实例化到Tomcat

    • 初始化,调用init()方法,可自定义内容

    • 处理请求调用service()方法

    • 销毁,服务器关闭自会调用destroy()方法,并释放资源

    ![image 3 6.png](Java Web 开发基础/image36.png)

  • Servlet开发

    • 创建JavaEE项目

    • 创建类实现Servlet接口(三种实现方式)

      • 实现Servlet接口

      • 继承GenericServlet(可用于其他请求的处理)

        GenericServlet 是 Servlet 接口的实现类,提供了部分方法的默认实现。它抽象化了请求的协议细节,因此适合开发非 HTTP 协议的 Servlet,也简化了代码书写。

        特点:

        • 实现了 Servlet 接口的所有方法,除了 service() 方法。
        • 常用于不依赖 HTTP 的协议,例如 FTP 或其他自定义协议。
      • ==继承HttpServlet==

    • 配置地址映射(多种方式)

      • 使用web.xml文件
      <servlet>
      		<servlet-name>自定义</servlet-name>
      		<servlet-class>类的全限定名</servlet-class>
      </servlet>
      
      <servlet-mapping>
      		<servlet-name>与自定义对应</servlet-name>
      		<url-pattern>/hello</url-pattern>
      </servlet-mapping>
      
    • 使用注解的方式:

      • @WebServlet("/hello") 等同于 xml配置
        • 通配符:@WebServlet("/*")@WebServlet("/*.html")
        • 多路径:@WebServlet({"/login", "/signin"})
        • 启动设置:@WebServlet(value = "/test",``**loadOnStartup** = 1) 这样服务器启动时直接加载,值是优先级
    • 处理post请求

      @Override
      protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          //首先设置一下响应类型
          resp.setContentType("text/html;charset=UTF-8");
          //获取POST请求携带的表单数据
          Map<String, String[]> map = req.getParameterMap();
          //判断表单是否完整
          if(map.containsKey("username") && map.containsKey("password")) {
              String username = req.getParameter("username");
              String password = req.getParameter("password");
      
              //权限校验(待完善)
          }else {
              resp.getWriter().write("错误,您的表单数据不完整!");
          }
      }
      
    • 文件下载、上传

      @WebServlet("/file")
      public class FileServlet extends HttpServlet {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.setContentType("image/png");
            ServletOutputStream outputStream = resp.getOutputStream();
            //将文件往流输出即可...
          }
          @Override
          protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              Part part = req.getPart("表单名称");
              InputStream inputStream = part.getInputStream();
              //将上传文件接收即可....
          }
      
      }
      
    • 重定向

      resp.sendRedirect("https://chat.deepseek.com/");
      
    • 请求转发,==只能项目内部处理==,并且方法类型一致

      req.getRequestDispatcher("/hh").forward(req, resp);;
      
    • 常用类

      • ServletConfig:每个Servlet对应的配置数据
      • ServletContextgetServletContext()所有的Servlet公用的配置
        • getServletContext() 获取

        • 数据共享:context.setAtrribute(”key”,obj)

        • 请求转发:

          context.getRequestDispatcher("/time").forward(req,resp);
          
  • maven项目启动Servlet

    • 下载Tomcat

    • 开maven项目

      • 导入servlet依赖

        <dependency>
              <groupId>jakarta.servlet</groupId>
              <artifactId>jakarta.servlet-api</artifactId>
              <version>5.0.0</version> <!-- 使用适合的版本 -->
              <scope>provided</scope><!-- 只在编译和测试时需要 -->
        </dependency>
        
    • 设置facet(将项目标记为web项目)

      ![image 4 6.png](Java Web 开发基础/image46.png)

    • 对项目进行打包

      ![image 5 5.png](Java Web 开发基础/image55.png)

    • 动态部署到Tomcat

      ![image 6 3.png](Java Web 开发基础/image63.png)

      • 调整url

      ![image 7 3.png](Java Web 开发基础/image73.png)

      关于facet和artifact

      ![image 8 3.png](Java Web 开发基础/image83.png)

  • 作用:==保持会话、跟踪用户状态==

Cookie

  • 浏览器访问服务器,服务器向浏览器发送的一段数据,浏览器保持并每次请求都会携带cookie

  • 通过这段数据用来识别用户,这就是cookie

  • 应用:记住我功能(设置cookie失效)

  • 使用

    Cookie cookie = new Cookie("test", "yyds");
    resp.addCookie(cookie);
    resp.sendRedirect("time");
    
    for (Cookie cookie : req.getCookies()) {
        System.out.println(cookie.getName() + ": " + cookie.getValue());
    }
    
    • 一些属性
      • name - Cookie的名称,Cookie一旦创建,名称便不可更改
      • value - Cookie的值,如果值为Unicode字符,需要为字符编码。如果为二进制数据,则需要使用BASE64编码
      • maxAge - Cookie失效的时间,单位秒。如果为正数,则该Cookie在maxAge秒后失效。如果为负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。如果为0,表示删除该Cookie。默认为-1。
      • secure - 该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。
      • path - Cookie的使用路径。如果设置为“/sessionWeb/”,则只有contextPath为“/sessionWeb”的程序可以访问该Cookie。如果设置为“/”,则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为“/”。
      • domain - 可以访问该Cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.”。
      • comment - 该Cookie的用处说明,浏览器显示Cookie信息的时候显示该说明。
      • version - Cookie使用的版本号。0表示遵循Netscape的Cookie规范,1表示遵循W3C的RFC 2109规范

Session

  • 浏览器每次请求服务器时,服务器会创建一个Session对象,表示该对话,与WebSocket的session不同

  • 原理:(依赖于cookie)在http请求上的应用,使用session==tomcat会自动将SessionId通过cookie返回给浏览器==,通过这样保证session与浏览器匹配

  • 使用:保存登录状态

    HttpSession session = req.getSession();
    session.setAttribute("user", user);
    
    HttpSession session = req.getSession();
    User user = (User) session.getAttribute("user");
    if(user == null) {
    resp.sendRedirect("login");
    return;
    }
    
    • session也有过期时间,默认30分钟,即使sessionID存在

      session.invalidate();
      

[!important] session并不依赖cookie,如果cookie被禁用,服务器可以通过响应体返回

Filter

  • 流程,放行静态资源

    ![image 9 3.png](Java Web 开发基础/image93.png)

    ![image 10 3.png](Java Web 开发基础/image103.png)

  • 使用流程,创建过滤器类实现,只需要实现Filter/HttpFilter/接口(类似Servlet),并添加@WebFilter("/*")注解即可

    @WebFilter("/*")   //路径的匹配规则和Servlet一致,这里表示匹配所有请求
    public class TestFilter implements Filter {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
             System.out.println("进入过滤器");
            //放行
            filterChain.doFilter(servletRequest, servletResponse);
            System.out.println("出过滤器");
        }
    }
    
    • 多个·过滤器的过滤顺序是按照==类名的自然排序进行==

Listener

  • 监听器,监听事件发生(==不常用==

  • 常见监听器,一种是创建销毁的、一种是属性变化

    关于ServletContext 关于HttpSession 关于ServletRequest
    ServletContextListener HttpSessionListener ServletRequestListener
    ServletContextAttributeListener HttpSessionAtrributeListener ServletRequestAtrributeListener
  • 演示

    @WebListener
    public class TestListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent se) {
    System.out.println("有一个Session被创建了");
    }
    }
    

Tomcat类加载机制

为什么我们写好的servlet编译好,放到Tomcat的文件夹下就可以进行运行?

  • 规范了开发者的文件布局,并指定了放在那个文件夹下
  • Tomcat实现了一套自己的类加载器,启动后类加载器将文件夹下的class进行加载,包括依赖包,并互相进行隔

它启动时构建出这样一棵类加载器树

Bootstrap ClassLoader
  └── System ClassLoader
        └── Common ClassLoader
              ├── Shared ClassLoader
              └── WebAppClassLoader (每个 web 应用独立一套)

📌 WebAppClassLoader 的职责:

  • 加载 WEB-INF/classes/ 下的所有 .class 文件
  • 加载 WEB-INF/lib/ 下所有 jar 包
  • 保证每个 web 应用的类互相隔离

所以你会发现:

A 项目里的类不会影响 B 项目,即便 jar 重名,版本不同也互不冲突。

  • 📌 2️⃣ 类命名空间的本质:

    在 JVM 里,一个类由:

    Class对象 = (ClassLoader实例 + 类的全限定名)
    

    唯一确定。

    所以:

    • A类 由 ClassLoaderA 加载 和 A类 由 ClassLoaderB 加载
    • 即使字节码一样,JVM 也视为两个不同的 Class 实例
  • 将class文件变成了一个个class类

  • 手写小demo

    好!你这个需求就是手写一个自定义 ClassLoader + 简单的命令行容器,我来给你直接做个可跑 demo,完全满足你的需求:


    📖 📦 功能:

    • A.javaB.java 编译好放到某个文件夹(比如 ./app-classes/
    • 写个自定义 ClassLoader 动态加载目录下 class 文件
    • 控制台输入 A → 调用 A 的 main 方法
    • 输入 B → 调用 B 的 main 方法

    📦 目录结构:

    项目/
    ├── ManagerApp.java      # 管理容器
    ├── MyClassLoader.java   # 自定义 ClassLoader
    ├── app-classes/         # 存放编译好的 A.class、B.class
    │   ├── A.class
    │   └── B.class
    

    📌 1️⃣ A.java / B.java(单独写好,放在 app-classes 目录,编译成 .class

    A.java

    public class A {
        public static void main(String[] args) {
            System.out.println("A");
        }
    }
    

    B.java

    public class B {
        public static void main(String[] args) {
            System.out.println("B");
        }
    }
    

    编译:

    javac A.java B.java
    # 放到 app-classes/
    

    📌 2️⃣ 自定义 ClassLoader

    MyClassLoader.java

    import java.io.*;
    
    public class MyClassLoader extends ClassLoader {
        private String classDir;
    
        public MyClassLoader(String classDir) {
            this.classDir = classDir;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                // 读取 .class 文件字节
                String filePath = classDir + name + ".class";
                FileInputStream fis = new FileInputStream(filePath);
                byte[] bytes = fis.readAllBytes();
                fis.close();
                // 定义类
                return defineClass(name, bytes, 0, bytes.length);
            } catch (IOException e) {
                throw new ClassNotFoundException("找不到类: " + name);
            }
        }
    }
    

    📌 3️⃣ 管理容器主程序

    ManagerApp.java

    import java.lang.reflect.Method;
    import java.util.Scanner;
    
    public class ManagerApp {
        public static void main(String[] args) throws Exception {
            String classDir = "./app-classes/";  // class 文件目录
            MyClassLoader loader = new MyClassLoader(classDir);
    
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入类名 (A/B)");
    
            while (true) {
                String input = scanner.nextLine();
                if ("exit".equalsIgnoreCase(input)) {
                    break;
                }
    
                try {
                    // 动态加载类
                    Class<?> clazz = loader.loadClass(input);
                    // 反射调用 main 方法
                    Method mainMethod = clazz.getMethod("main", String[].class);
                    mainMethod.invoke(null, (Object) new String[]{});
                } catch (Exception e) {
                    System.out.println("执行出错:" + e.getMessage());
                }
            }
    
            System.out.println("已退出");
        }
    }
    

    📌 4️⃣ 运行方式

    1️⃣ 编译 A、B

    javac A.java B.java
    

    放入 ./app-classes/

    2️⃣ 编译管理器:

    javac MyClassLoader.java ManagerApp.java
    

    3️⃣ 运行:

    java ManagerApp
    

    👉 输入:

    A
    

    输出:

    A
    

    👉 再输入:

    B
    

    输出:

    B
    

    📖 总结:

    你现在就实现了:

    • 手写 ClassLoader 动态加载 class 文件
    • 动态执行 main 方法
    • 控制台命令动态调度

    📌 扩展进阶:

    你现在这就是 Tomcat 的 WebAppClassLoader 的简化版!

    要不要我再帮你做个多线程执行+ThreadLocal隔离+热更新 class 文件版?要就说一声👌

在这套规则下,才让我们可以编写servlet然后直接部署,通过tomcat管理

JSP 编程

  • 动态Web页面,基本上已经淘汰,js足够强大,不需要因为部分数据的更新,而更新整个页面

  • 结构:html+java 结合(<% … %>)

  • 本质:

    • JSP ——> Servlet

    • JSP 通过JSP 编译器 生成Servlet (继承自 HttpServlet),调用javac对Servlet进行编译,后使用反射进行加载

    • 运行时调用Java编译器

      在 Java 程序中调用 javac 来编译 Java 文件,通常是通过执行系统命令来实现的。可以使用 java.lang.ProcessBuilderRuntime.getRuntime().exec() 来执行 javac 命令。这些方法允许 Java 程序启动一个外部进程(在这种情况下是 javac 编译器)来编译 Java 源文件。

      1. 使用 **ProcessBuilder** 执行 **javac** 编译命令

      ProcessBuilder 是 Java 提供的一种启动和管理外部进程的工具类。通过它,你可以设置命令参数,并捕获命令执行的输出和错误信息。

      示例代码:

      import java.io.*;
      import java.util.*;
      
      public class JavacCompiler {
          public static void main(String[] args) {
              try {
                  // 指定 javac 命令路径,假设 javac 已经在系统的 PATH 中
                  List<String> command = new ArrayList<>();
                  command.add("javac");  // 运行 javac 编译器
                  command.add("HelloWorld.java");  // 要编译的 Java 文件
      
                  // 创建 ProcessBuilder 并执行命令
                  ProcessBuilder processBuilder = new ProcessBuilder(command);
                  processBuilder.redirectErrorStream(true); // 合并标准输出和错误输出
      
                  // 启动进程并执行 javac
                  Process process = processBuilder.start();
      
                  // 获取编译过程的输出
                  BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                  String line;
                  while ((line = reader.readLine()) != null) {
                      System.out.println(line); // 输出编译过程中的信息
                  }
      
                  // 等待编译完成
                  int exitCode = process.waitFor();
                  if (exitCode == 0) {
                      System.out.println("Compilation successful.");
                  } else {
                      System.out.println("Compilation failed.");
                  }
      
              } catch (IOException | InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
      
      

      解释:

      • **ProcessBuilder**:创建一个外部进程来执行 javac 命令。
      • **command.add("javac")**:指定要执行的命令,即 javac
      • **command.add("HelloWorld.java")**:指定要编译的 Java 文件。
      • **process.waitFor()**:等待外部进程执行完毕(即 javac 编译完 Java 文件)。
      • **process.getInputStream()**:获取编译过程中的输出信息(如错误信息或警告)。

      如果编译成功,程序会输出 Compilation successful,否则会输出 Compilation failed

      2. 使用 **Runtime.getRuntime().exec()** 执行 **javac** 编译命令

      Runtime.getRuntime().exec() 方法也可以用来启动外部进程,执行 javac 编译命令。

      示例代码:

      import java.io.*;
      
      public class JavacCompiler {
          public static void main(String[] args) {
              try {
                  // 执行 javac 编译命令
                  String command = "javac HelloWorld.java";
                  Process process = Runtime.getRuntime().exec(command);
      
                  // 获取编译过程的输出
                  BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                  String line;
                  while ((line = reader.readLine()) != null) {
                      System.out.println(line); // 输出编译过程中的信息
                  }
      
                  // 等待编译完成
                  int exitCode = process.waitFor();
                  if (exitCode == 0) {
                      System.out.println("Compilation successful.");
                  } else {
                      System.out.println("Compilation failed.");
                  }
      
              } catch (IOException | InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
      
      

      解释:

      • **Runtime.getRuntime().exec(command)**:执行命令 javac HelloWorld.java 启动编译进程。
      • **process.waitFor()**:等待 javac 进程执行完毕。
      • **process.getInputStream()**:获取编译的输出,便于显示或调试。

      3. 处理编译过程中出现的错误

      javac 编译器在编译时,可能会输出错误或警告信息。你可以通过读取进程的错误流 (process.getErrorStream()) 来捕获并处理这些信息。

      修改后的代码(捕获错误流):

      import java.io.*;
      import java.util.*;
      
      public class JavacCompiler {
          public static void main(String[] args) {
              try {
                  // 指定 javac 命令路径,假设 javac 已经在系统的 PATH 中
                  List<String> command = new ArrayList<>();
                  command.add("javac");
                  command.add("HelloWorld.java");
      
                  ProcessBuilder processBuilder = new ProcessBuilder(command);
                  processBuilder.redirectErrorStream(true); // 合并标准输出和错误输出
      
                  Process process = processBuilder.start();
      
                  // 获取编译过程的输出和错误
                  BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                  String line;
                  while ((line = reader.readLine()) != null) {
                      System.out.println(line); // 输出编译过程中的信息或错误
                  }
      
                  int exitCode = process.waitFor();
                  if (exitCode == 0) {
                      System.out.println("Compilation successful.");
                  } else {
                      System.out.println("Compilation failed.");
                  }
      
              } catch (IOException | InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
      

      4. 指定多个文件或目录进行编译

      如果你想编译多个 Java 文件或者整个目录中的所有 Java 文件,可以这样做:

      import java.io.*;
      public class JavacCompiler {
          public static void main(String[] args) {
              try {
                  // 编译当前目录下的所有 Java 文件
                  String command = "javac *.java";
                  Process process = Runtime.getRuntime().exec(command);
      
                  BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                  String line;
                  while ((line = reader.readLine()) != null) {
                      System.out.println(line);  // 输出编译过程中的信息
                  }
      
                  int exitCode = process.waitFor();
                  if (exitCode == 0) {
                      System.out.println("Compilation successful.");
                  } else {
                      System.out.println("Compilation failed.");
                  }
      
              } catch (IOException | InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
      

      5. 编译指定的源文件和类路径

      如果你有外部的依赖或者类路径需要指定,可以在 javac 命令中传递 **-cp****-classpath** 参数。

      例如,假设需要编译 HelloWorld.java 并指定类路径:

      String command = "javac -cp /path/to/libs/* HelloWorld.java";
      

      总结:

      1. **ProcessBuilder****Runtime.getRuntime().exec()** 是在 Java 程序中调用外部命令(如 javac 编译器)的常用方法。
      2. **javac** 编译器 会将 Java 源代码文件(.java)编译成字节码文件(.class)。
      3. 可以通过 **ProcessBuilder****exec()** 启动一个外部进程来执行 javac 命令,并通过输入流和输出流处理编译过程中的信息。

      这种方式对于 Java 程序动态编译其他 Java 文件非常有效,尤其是在某些工具或者自动化编译系统中。