27 KiB
title, date, tags
| title | date | tags | |
|---|---|---|---|
| Java Web 开发基础 | 2025-01-26 |
|
Java Web 开发基础
-
C/S架构(Client/Server)
- 优点:
- 高性能、用户体验好
- 缺点:
- 需要安装且不方便更新
- 不方便维护
- 二层:客户端、服务器
- 优点:
-
B/S架构(Browser/Server)
- 优点:
- 不需要安装、使用方便
- 更新方便
- 缺点:
- 性能有一点限制
- 三层
- 表现层
- 业务逻辑层
- 数据访问层
- 优点:
-
Web 开发相关知识
-
静态web资源:固定的文件,如视频、图片、文件等
- 只需要web服务器处理

-
动态web资源:根据用户要求动态生成的内容,所以依赖服务器的处理
- 需要处理需求的代码
- 返回json资源、生成的网页(jsp)

这种开发方式是因为之前的javaScript的发展不成熟,浏览器自身性能不足、对js的支持不统一、异步请求不成熟,所以动态页面全都靠生成响应
[!important] Web应用开发好后,若想供外界访问,需要把web应用所在目录交给web服务器管理,这个过程称之为虚似目录的映射

-

了解Tomcat
- 文件位置
- bin中有启动程序
- config中有配置文件
- 项目部署
- 将项目打包成war包,放在webapps中,并启动即可
- 静态文件有默认的servlet进行响应
- 配置信息
- 乱码配置
- 管理配置
Servlet 编程
-
基本作用
- 处理网络请求类
- 接收请求
- 响应内容
-
生命周期:每个请求对应一个线程
-
<mar每个请求对应一个线程

-
加载并实例化到Tomcat
-
初始化,调用
init()方法,可自定义内容 -
处理请求调用
service()方法 -
销毁,服务器关闭自会调用
destroy()方法,并释放资源

-
-
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对应的配置数据
- ServletContext:getServletContext()所有的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项目)

-
对项目进行打包

-
动态部署到Tomcat

- 调整url

关于facet和artifact

-
Cookie 与 Session
- 作用:==保持会话、跟踪用户状态==
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
-
流程,放行静态资源


-
使用流程,创建过滤器类实现,只需要实现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.java、B.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.java3️⃣ 运行:
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.ProcessBuilder或Runtime.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";总结:
**ProcessBuilder**和**Runtime.getRuntime().exec()**是在 Java 程序中调用外部命令(如javac编译器)的常用方法。**javac**编译器 会将 Java 源代码文件(.java)编译成字节码文件(.class)。- 可以通过
**ProcessBuilder**或**exec()**启动一个外部进程来执行javac命令,并通过输入流和输出流处理编译过程中的信息。
这种方式对于 Java 程序动态编译其他 Java 文件非常有效,尤其是在某些工具或者自动化编译系统中。
-