JavaWeb 实战开发指南
JavaWeb 是基于 Java 技术栈的 Web 应用开发,涵盖 Servlet、JSP、Spring、MyBatis 等核心技术。在企业级开发中,Spring Boot 已成为主流框架,简化了配置,提高了开发效率。
核心原则:JavaWeb 开发遵循 MVC 分层架构,关注点分离,代码复用,提高可维护性。
通过本指南,你将掌握 JavaWeb 开发中的常见问题解决方案、关键技术点注意事项以及最佳实践,避免踩坑,提升开发效率。
痛点与解决方案
在实际开发中,经常会遇到以下问题:
痛点与解决方案
核心功能概览
JavaWeb 开发的核心技术点:
核心功能概览
Servlet 生命周期
★★★★☆Spring MVC 参数绑定
★★★★★MyBatis 映射机制
★★★★★Spring 事务管理
★★★★★Session/Cookie/JWT
★★★★★Ajax 异步请求
★★★★★文件上传下载
★★★☆☆异常处理机制
★★★★☆技术点详解
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 失效场景:
- 同类方法调用失效:
@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(); // ✅ 通过代理对象调用,事务生效
}
}
- 检查异常不回滚:
@Transactional // 默认只回滚 RuntimeException
public void update() throws Exception {
// 如果抛出 Exception,不会回滚
}
// ✅ 解决方案:指定回滚异常
@Transactional(rollbackFor = Exception.class)
public void update() throws Exception {
// 抛出任何异常都会回滚
}
- 方法必须是 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 机制
工作原理:
- 用户登录,服务器创建 Session 并存储用户信息
- 返回 SessionId 给客户端(通过 Cookie 或 URL 参数)
- 客户端后续请求携带 SessionId
- 服务器根据 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 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 和实体类:
- 扫描 Mapper 接口:通过
@Mapper注解或@MapperScan扫描包路径 - 匹配 XML 文件:根据接口全限定名(如
com.example.mapper.UserMapper)找到对应的 XML 文件(如UserMapper.xml) - 方法名匹配 SQL:接口方法名
findById对应 XML 中的<select id="findById"> - 返回值映射:
resultType或resultMap指定返回的实体类(如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>
实体类与数据库字段映射过程:
- MyBatis 根据
resultType="User"找到实体类(通过typeAliases配置或全限定名) - 默认使用驼峰命名转换:实体类字段
userName→ 数据库字段user_name - 如果字段名不匹配,需要使用
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();
}
实战场景
以下是一些典型的企业级应用场景:
实战场景
用户登录(Session/Cookie/JWT)
登录接口 → Session 存储 / Cookie 携带 / JWT Token
分页查询(MyBatis + Ajax)
PageHelper + MyBatis + 前端 Ajax
文件上传(Spring Boot)
MultipartFile + 配置限制
数据导出(Excel/CSV)
POI/EasyExcel + 响应流
编码规范与最佳实践
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)
如有疑问,欢迎交流讨论!