JavaWeb 实战开发指南

JavaWeb 是基于 Java 技术栈的 Web 应用开发,涵盖 Servlet、JSP、Spring、MyBatis 等核心技术。在企业级开发中,Spring Boot 已成为主流框架,简化了配置,提高了开发效率。

核心原则:JavaWeb 开发遵循 MVC 分层架构,关注点分离,代码复用,提高可维护性。

通过本指南,你将掌握 JavaWeb 开发中的常见问题解决方案、关键技术点注意事项以及最佳实践,避免踩坑,提升开发效率。


痛点与解决方案

在实际开发中,经常会遇到以下问题:

痛点与解决方案

中文乱码问题(GET/POST 请求、数据库、页面显示)
页面显示乱码 → 用户输入保存错误 → 数据丢失
解决方案:统一使用 UTF-8 编码(request.setCharacterEncoding、response.setContentType、数据库连接 URL 添加 useUnicode=true&characterEncoding=utf8)
SQL 注入漏洞
数据库被恶意操作 → 数据泄露/删除 → 系统被攻击
解决方案:使用 PreparedStatement 预编译语句,禁止字符串拼接 SQL
事务管理不当(@Transactional 失效)
数据不一致 → 业务逻辑错误 → 数据丢失
解决方案:同类方法调用失效(需通过代理对象调用)、检查异常不回滚(使用 rollbackFor)、方法必须是 public
文件上传内存溢出
大文件上传导致内存溢出 → 服务器崩溃
解决方案:配置 MultipartFile 最大大小(spring.servlet.multipart.max-file-size)、使用流式处理
Session 共享问题(分布式部署)
用户登录状态丢失 → 需要重复登录 → 用户体验差
解决方案:使用 Redis 存储 Session 或 JWT Token 方案
Servlet 线程安全问题
多用户数据混乱 → 业务逻辑错误
解决方案:避免使用成员变量、使用 ThreadLocal 或方法局部变量
MyBatis N+1 查询问题
数据库查询次数过多 → 性能下降 → 响应慢
解决方案:使用 <resultMap> 配置关联查询、或者使用 <collection> 标签一次性查询
跨域问题(CORS)
前端 Ajax 请求被浏览器拦截 → 功能无法使用
解决方案:后端配置 @CrossOrigin 或配置 CorsFilter、前端使用代理

核心功能概览

JavaWeb 开发的核心技术点:

核心功能概览

Servlet 生命周期

★★★★☆
适用场景:理解请求处理流程、优化性能
init() → service() → destroy()
注意:单例模式,线程不安全,避免使用成员变量

Spring MVC 参数绑定

★★★★★
适用场景:自动将请求参数绑定到方法参数
@RequestParam、@PathVariable、@RequestBody
注意:支持基本类型、对象、集合自动绑定

MyBatis 映射机制

★★★★★
适用场景:SQL 与 Java 对象自动映射
Mapper 接口 + XML/注解、resultMap、typeAliases
注意:通过方法名和 XML id 匹配,返回值类型匹配实体类

Spring 事务管理

★★★★★
适用场景:保证数据一致性、原子性
@Transactional(propagation, isolation, rollbackFor)
注意:同类调用失效、仅对 public 方法生效

Session/Cookie/JWT

★★★★★
适用场景:用户身份认证、状态保持
Session:服务器存储 / Cookie:客户端存储 / JWT:无状态 Token
注意:分布式环境用 JWT、Session 有大小限制

Ajax 异步请求

★★★★★
适用场景:前后端数据交互、无刷新更新
$.ajax()、fetch()、axios.post()
注意:注意跨域问题、错误处理、异步回调

文件上传下载

★★★☆☆
适用场景:用户文件管理
MultipartFile、设置文件大小限制、路径安全检查
注意:防止路径遍历攻击、限制文件类型和大小

异常处理机制

★★★★☆
适用场景:统一异常处理、提升用户体验
@ControllerAdvice + @ExceptionHandler
注意:全局异常捕获、返回统一错误格式

技术点详解

Servlet 基础(简要)

Servlet 是 JavaWeb 的核心,运行在服务器端,处理客户端请求。

生命周期

  • init():初始化(仅执行一次)
  • service():处理请求(每次请求都执行)
  • destroy():销毁(服务器关闭时执行)

⚠️ 重要注意事项

  • Servlet 是单例模式,多个请求共享同一个实例
  • 避免使用成员变量,会导致线程安全问题
  • 使用局部变量ThreadLocal 存储请求相关数据
// ❌ 错误示例:线程不安全
public class BadServlet extends HttpServlet {
    private String username; // 成员变量,多线程共享
}

// ✅ 正确示例:使用局部变量
public class GoodServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username"); // 局部变量,线程安全
    }
}

Spring Boot 核心要点

1. 自动配置原理

Spring Boot 通过 @SpringBootApplication 注解实现自动配置:

  • @EnableAutoConfiguration:启用自动配置
  • @ComponentScan:扫描组件
  • @Configuration:配置类

常见配置

# application.yml
spring:
  servlet:
    multipart:
      max-file-size: 10MB  # 文件上传大小限制
      max-request-size: 10MB
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
    username: root
    password: 123456

2. 事务管理(重点)

@Transactional 失效场景

  1. 同类方法调用失效
@Service
public class UserService {
    public void method1() {
        method2(); // ❌ 不会开启事务,因为是通过 this 调用
    }
    
    @Transactional
    public void method2() {
        // 事务不会生效
    }
}

// ✅ 解决方案:通过注入自身或提取到另一个 Service
@Service
public class UserService {
    @Autowired
    private UserService userService; // 注入自身
    
    public void method1() {
        userService.method2(); // ✅ 通过代理对象调用,事务生效
    }
}
  1. 检查异常不回滚
@Transactional // 默认只回滚 RuntimeException
public void update() throws Exception {
    // 如果抛出 Exception,不会回滚
}

// ✅ 解决方案:指定回滚异常
@Transactional(rollbackFor = Exception.class)
public void update() throws Exception {
    // 抛出任何异常都会回滚
}
  1. 方法必须是 public
@Transactional
private void update() { } // ❌ 私有方法,事务不生效

@Transactional
public void update() { } // ✅ 公共方法,事务生效

3. 跨域配置

@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*"); // 允许所有域名
        config.addAllowedHeader("*"); // 允许所有请求头
        config.addAllowedMethod("*"); // 允许所有请求方法
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

Session、Cookie 与 JWT

Session 机制

工作原理

  1. 用户登录,服务器创建 Session 并存储用户信息
  2. 返回 SessionId 给客户端(通过 Cookie 或 URL 参数)
  3. 客户端后续请求携带 SessionId
  4. 服务器根据 SessionId 查找对应的 Session
// 存储 Session
HttpSession session = request.getSession();
session.setAttribute("user", user);

// 获取 Session
User user = (User) session.getAttribute("user");

// Session 失效
session.invalidate();

⚠️ 注意事项

  • Session 默认 30 分钟无活动自动失效
  • 分布式环境下需要 Session 共享(Redis)
  • Session 存储在服务器内存,占用资源
// 设置 Cookie
Cookie cookie = new Cookie("username", "admin");
cookie.setMaxAge(3600); // 1小时过期
cookie.setPath("/"); // 路径
response.addCookie(cookie);

// 获取 Cookie
Cookie[] cookies = request.getCookies();
for (Cookie c : cookies) {
    if ("username".equals(c.getName())) {
        String value = c.getValue();
    }
}

Cookie vs Session

  • Cookie:存储在客户端,大小限制 4KB,可能被禁用
  • Session:存储在服务器,大小无限制,依赖 Cookie 传递 SessionId

JWT(JSON Web Token)无状态方案

为什么需要 JWT

  • 分布式环境下 Session 共享复杂
  • 服务器扩展时 Session 同步困难
  • JWT 无状态,适合微服务架构

JWT 组成

  • Header:算法和类型
  • Payload:用户信息
  • Signature:签名,防止篡改
// 引入依赖:io.jsonwebtoken:jjwt
@Service
public class JwtService {
    private String secret = "your-secret-key";
    private long expiration = 3600000; // 1小时
    
    // 生成 Token
    public String generateToken(User user) {
        return Jwts.builder()
            .setSubject(user.getUsername())
            .setExpiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
    
    // 验证 Token
    public Claims parseToken(String token) {
        return Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
    }
}

// Controller 中使用
@RestController
public class AuthController {
    @Autowired
    private JwtService jwtService;
    
    @PostMapping("/login")
    public Result login(@RequestBody LoginRequest request) {
        // 验证用户名密码
        User user = userService.authenticate(request);
        String token = jwtService.generateToken(user);
        return Result.success(token);
    }
    
    // 前端将 Token 放在请求头:Authorization: Bearer <token>
}

JWT 优缺点

  • ✅ 优点:无状态、跨域友好、适合分布式
  • ❌ 缺点:Token 无法主动失效(需配合 Redis)、Payload 不能太大

MyBatis 映射机制详解

1. Mapper 接口与 XML 的映射关系

MyBatis 如何找到 Mapper 和实体类

  1. 扫描 Mapper 接口:通过 @Mapper 注解或 @MapperScan 扫描包路径
  2. 匹配 XML 文件:根据接口全限定名(如 com.example.mapper.UserMapper)找到对应的 XML 文件(如 UserMapper.xml
  3. 方法名匹配 SQL:接口方法名 findById 对应 XML 中的 <select id="findById">
  4. 返回值映射resultTyperesultMap 指定返回的实体类(如 User
// Mapper 接口 - 定义在 com.example.mapper 包下
@Mapper
public interface UserMapper {
    User findById(Long id);  // 方法名 = XML 中的 id
    List<User> findAll();
}
<!-- resources/mapper/UserMapper.xml -->
<!-- namespace 必须与接口全限定名一致 -->
<mapper namespace="com.example.mapper.UserMapper">
    <!-- id = 方法名 findById -->
    <!-- resultType = 方法返回类型 User(需配置 typeAliases 或写全限定名) -->
    <select id="findById" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>

实体类与数据库字段映射过程

  1. MyBatis 根据 resultType="User" 找到实体类(通过 typeAliases 配置或全限定名)
  2. 默认使用驼峰命名转换:实体类字段 userName → 数据库字段 user_name
  3. 如果字段名不匹配,需要使用 resultMap 手动映射
// 实体类
public class User {
    private Long id;
    private String userName;  // 驼峰命名
    private Integer userAge;
}
<!-- 自动映射(启用驼峰命名转换) -->
<!-- application.yml 配置:mybatis.configuration.map-underscore-to-camel-case=true -->
<select id="findById" resultType="User">
    SELECT id, user_name, user_age FROM user WHERE id = #{id}
</select>
<!-- 自动映射:user_name → userName, user_age → userAge -->

<!-- 手动映射(字段名不一致时) -->
<resultMap id="userMap" type="User">
    <id property="id" column="user_id"/>        <!-- 主键映射 -->
    <result property="userName" column="user_name"/>  <!-- 字段映射 -->
    <result property="userAge" column="user_age"/>
</resultMap>

```xml
<!-- 自动映射(驼峰转换) -->
<select id="findById" resultType="User">
    SELECT user_name, user_age FROM user WHERE id = #{id}
</select>
<!-- User 实体类:userName, userAge -->

<!-- 手动映射(resultMap) -->
<resultMap id="userMap" type="User">
    <id property="id" column="user_id"/>
    <result property="userName" column="user_name"/>
    <result property="userAge" column="user_age"/>
</resultMap>

<select id="findById" resultMap="userMap">
    SELECT user_id, user_name, user_age FROM user WHERE id = #{id}
</select>

2. 常见映射场景

一对多映射(避免 N+1 查询):

<!-- User 和 Order 一对多关系 -->
<resultMap id="userWithOrders" type="User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <!-- collection 标签一次性查询所有订单 -->
    <collection property="orders" ofType="Order">
        <id property="id" column="order_id"/>
        <result property="orderNo" column="order_no"/>
    </collection>
</resultMap>

<select id="findUserWithOrders" resultMap="userWithOrders">
    SELECT u.id, u.username, o.id as order_id, o.order_no
    FROM user u
    LEFT JOIN order o ON u.id = o.user_id
    WHERE u.id = #{id}
</select>

动态 SQL

<select id="findByCondition" resultType="User">
    SELECT * FROM user
    <where>
        <if test="username != null and username != ''">
            AND username LIKE #{username}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>

3. MyBatis 常见坑

⚠️ N+1 查询问题

// ❌ 错误:会执行 N+1 次查询
List<User> users = userMapper.findAll();
for (User user : users) {
    List<Order> orders = orderMapper.findByUserId(user.getId()); // N次查询
}

// ✅ 正确:使用关联查询,一次查询完成
<resultMap id="userWithOrders" type="User">
    <collection property="orders" ofType="Order" select="findOrdersByUserId"/>
</resultMap>

⚠️ 参数绑定

<!-- ${} 直接拼接,有 SQL 注入风险 -->
SELECT * FROM user WHERE name = '${name}' <!-- ❌ 危险 -->

<!-- #{} 预编译,安全 -->
SELECT * FROM user WHERE name = #{name} <!-- ✅ 安全 -->

Ajax 异步请求(前端重点)

基本使用

// jQuery 方式
$.ajax({
    url: '/api/user/list',
    type: 'GET',
    dataType: 'json',
    success: function(data) {
        console.log(data);
    },
    error: function(xhr, status, error) {
        console.error('请求失败:', error);
    }
});

// Fetch API(现代浏览器)
fetch('/api/user/list')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('请求失败:', error));

// Axios(推荐)
axios.get('/api/user/list')
    .then(response => {
        console.log(response.data);
    })
    .catch(error => {
        console.error('请求失败:', error);
    });

跨域问题解决

后端配置(推荐):

@RestController
@CrossOrigin(origins = "*") // 允许所有域名
public class UserController {
    // ...
}

前端代理(开发环境):

// vite.config.js / webpack.config.js
proxy: {
    '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
    }
}

POST 请求提交 JSON

// 前端
axios.post('/api/user/save', {
    username: 'admin',
    password: '123456'
}, {
    headers: {
        'Content-Type': 'application/json'
    }
});

// 后端
@PostMapping("/user/save")
public Result save(@RequestBody User user) {
    // @RequestBody 自动将 JSON 转换为对象
    return Result.success();
}

实战场景

以下是一些典型的企业级应用场景:

实战场景

1

用户登录(Session/Cookie/JWT)

资源: 登录接口 → Session 存储 / Cookie 携带 / JWT Token
规则: Session:登录后存储到服务器,返回 SessionId 给 Cookie;JWT:生成 Token 返回前端,前端存储并每次请求携带
效果: Session 方案简单但不利于分布式,JWT 无状态适合分布式但需考虑 Token 刷新
2

分页查询(MyBatis + Ajax)

资源: PageHelper + MyBatis + 前端 Ajax
规则: PageHelper.startPage(pageNum, pageSize) 自动拦截,返回 PageInfo 对象,前端 Ajax 异步加载
效果: 提升用户体验,减少服务器压力,支持大数据量分页
3

文件上传(Spring Boot)

资源: MultipartFile + 配置限制
规则: 接收 MultipartFile 参数,检查文件类型和大小,保存到指定目录,返回文件路径
效果: 支持多种文件格式,防止恶意文件上传,控制服务器存储
4

数据导出(Excel/CSV)

资源: POI/EasyExcel + 响应流
规则: 查询数据 → 生成 Excel → 设置响应头 → 写入响应流
效果: 支持大批量数据导出,解决中文乱码问题

编码规范与最佳实践

1. 统一异常处理

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        return Result.error(e.getMessage());
    }
    
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error("系统异常,请联系管理员");
    }
}

2. 统一响应格式

@Data
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("成功");
        result.setData(data);
        return result;
    }
}

3. 参数校验

@PostMapping("/user/save")
public Result save(@Valid @RequestBody UserDTO userDTO) {
    // @Valid 触发校验
    return Result.success();
}

// DTO 中添加校验注解
public class UserDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @Min(value = 18, message = "年龄不能小于18岁")
    private Integer age;
}

4. 日志记录

@Slf4j
@RestController
public class UserController {
    
    @GetMapping("/user/{id}")
    public Result getUser(@PathVariable Long id) {
        log.info("查询用户, id: {}", id);
        // ...
    }
}

常见错误排查

1. 404 错误

  • 检查 Controller 的 @RequestMapping 路径
  • 检查请求方法和 Controller 方法是否匹配(GET/POST)
  • 检查 Spring Boot 启动类是否扫描到 Controller 包

2. 500 错误

  • 查看控制台错误日志
  • 检查 SQL 语句是否正确
  • 检查参数绑定是否正确(@RequestParam@PathVariable

3. 中文乱码

  • 确认数据库连接 URL 添加 useUnicode=true&characterEncoding=utf8
  • 确认请求和响应编码设置为 UTF-8
  • 确认前端页面编码为 UTF-8

4. 事务不生效

  • 检查方法是否为 public
  • 检查是否通过代理对象调用(同类调用失效)
  • 检查异常类型是否会被回滚

总结

JavaWeb 开发需要掌握 Servlet、Spring Boot、MyBatis 等核心技术,同时要注意线程安全、事务管理、编码规范等细节。

关键要点

  • 使用 PreparedStatement 防止 SQL 注入
  • @Transactional 同类调用失效,需注意
  • 分布式环境使用 JWT 代替 Session
  • MyBatis 使用 resultMap 避免 N+1 查询
  • 统一异常处理和响应格式
  • 注意跨域问题配置

接下来,你可以:

  • 尝试搭建一个完整的 Spring Boot + MyBatis 项目
  • 实现用户登录、文件上传等功能
  • 学习更多 Spring Boot 的高级特性(如 AOP、缓存等)
  • 探索微服务架构(Spring Cloud)

如有疑问,欢迎交流讨论!