ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手搓代码 vs AI编程:2小时实战构建Spring Boot待办事项API后端

手搓代码 vs AI编程:2小时实战构建Spring Boot待办事项API后端 1. 引子一次“较真”的代码对决最近在技术社区里关于“手搓代码”和“AI编程”谁更厉害的讨论就没停过。有人说AI是未来动动嘴皮子就能生成代码效率碾压也有人坚持没有扎实的手写功底AI生成的代码就是空中楼阁调试起来更费劲。作为一个写了十几年代码的老兵我决定不站队而是亲自下场用一场限时2小时的实战对决来探探两者的虚实。我选择的战场是一个中等复杂度的实际需求构建一个具备完整增删改查CRUD功能、带用户认证和简单数据可视化的待办事项TodoWeb应用后端。这个需求涵盖了现代Web开发中常见的模块设计、数据库交互、API构建和业务逻辑既有套路化的部分也有需要一点设计思考的地方。我的计划是第一个小时完全依靠我自己“手搓”代码从零开始设计数据库表、搭建项目框架、编写业务逻辑和API接口。第二个小时则交给以OpenAI Codex为代表的AI编程助手我实际使用了Cursor编辑器它深度集成了这类模型我通过自然语言描述需求让它来生成代码我负责审查、整合和微调。这不仅仅是一场速度的比拼更是对代码质量、可维护性、思维过程以及最终“人机协作”体验的一次深度审视。2小时后我得到的结论可能和很多人想的不太一样。2. 第一回合传统手艺——“手搓代码”的沉浸与掌控我打开了熟悉的IDE新建了一个Spring Boot项目。这是我的舒适区每一步都清晰明确。2.1 架构设计与环境搭建首先我需要规划技术栈。考虑到这是一个演示项目我选择了最经典的组合Spring Boot Spring Data JPA H2内存数据库方便测试 Spring Security for JWT认证。我快速通过Spring Initializr生成项目引入了必要的依赖。!-- pom.xml 片段 -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency !-- ... 其他依赖 -- /dependencies选择H2而不是MySQL是因为它无需安装配置启动即用非常适合快速原型开发。而JWTJSON Web Token是现代无状态API认证的事实标准比传统的Session更适用于前后端分离的场景。这些选择背后是基于对项目目标快速验证、现代架构和工具特性的理解。2.2 领域模型与数据层构建接下来是设计核心领域对象。一个Todo实体是显而易见的它包括ID、标题、描述、完成状态、创建时间、所属用户等字段。同时我们需要一个User实体来处理认证。// User.java Entity Table(name users) Data // 使用Lombok简化Getter/Setter NoArgsConstructor AllArgsConstructor public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(unique true, nullable false) private String username; Column(nullable false) private String password; // 实际存储应为加密后的哈希值 private String email; // 省略构造函数、Getter/Setter } // Todo.java Entity Table(name todos) Data NoArgsConstructor AllArgsConstructor public class Todo { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String title; private String description; private boolean completed; private LocalDateTime createdAt; ManyToOne JoinColumn(name user_id, nullable false) private User user; // 省略构造函数、Getter/Setter }这里有几个关键设计点1. 使用ManyToOne关联Todo和User一个用户可以有多个待办事项。2.password字段未来需要通过Spring Security的PasswordEncoder进行加密存储这里先留空。3. 使用LocalDateTime而非Date这是Java 8时间API的最佳实践。手写这些代码时我脑子里同步进行着数据库表结构映射、关联关系以及后续查询效率的预演。然后我创建了对应的Repository接口。Spring Data JPA让这步变得极其简单。// TodoRepository.java public interface TodoRepository extends JpaRepositoryTodo, Long { ListTodo findByUserId(Long userId); ListTodo findByUserIdAndCompleted(Long userId, boolean completed); } // UserRepository.java public interface UserRepository extends JpaRepositoryUser, Long { OptionalUser findByUsername(String username); boolean existsByUsername(String username); }我特意为TodoRepository添加了两个自定义查询方法。findByUserId用于获取某个用户的所有待办项这是核心需求。findByUserIdAndCompleted则可能用于前端筛选“已完成”或“未完成”项。这种基于方法名自动生成查询的能力是Spring Data JPA的精华手写时能让我精准地只暴露必要的查询接口避免过度泛化。2.3 业务逻辑与API层实现服务层Service是业务逻辑的核心。我创建了TodoService和AuthService。// TodoService.java Service Transactional RequiredArgsConstructor // Lombok注解为final字段生成构造函数 public class TodoService { private final TodoRepository todoRepository; private final UserRepository userRepository; public Todo createTodo(Long userId, CreateTodoRequest request) { User user userRepository.findById(userId) .orElseThrow(() - new EntityNotFoundException(User not found)); Todo todo new Todo(); todo.setTitle(request.getTitle()); todo.setDescription(request.getDescription()); todo.setCompleted(false); todo.setCreatedAt(LocalDateTime.now()); todo.setUser(user); return todoRepository.save(todo); } public ListTodo getUserTodos(Long userId) { return todoRepository.findByUserId(userId); } public Todo updateTodo(Long userId, Long todoId, UpdateTodoRequest request) { Todo todo todoRepository.findById(todoId) .orElseThrow(() - EntityNotFoundException(Todo not found)); // 权限校验确保待办项属于当前用户 if (!todo.getUser().getId().equals(userId)) { throw new AccessDeniedException(You can only update your own todos); } if (request.getTitle() ! null) todo.setTitle(request.getTitle()); if (request.getDescription() ! null) todo.setDescription(request.getDescription()); if (request.getCompleted() ! null) todo.setCompleted(request.getCompleted()); return todoRepository.save(todo); // Transactional 确保自动更新 } public void deleteTodo(Long userId, Long todoId) { Todo todo todoRepository.findById(todoId) .orElseThrow(() - new EntityNotFoundException(Todo not found)); if (!todo.getUser().getId().equals(userId)) { throw new AccessDeniedException(You can only delete your own todos); } todoRepository.delete(todo); } }在updateTodo和deleteTodo方法中我加入了关键的权限校验逻辑。这是业务安全性的基石确保用户只能操作自己的数据。手写代码时这种安全边界意识是自然而然的我会在编写每个对外接口时本能地问自己“这里有没有越权风险”控制器Controller层负责暴露HTTP API。我遵循RESTful风格进行设计。// TodoController.java RestController RequestMapping(/api/todos) RequiredArgsConstructor public class TodoController { private final TodoService todoService; PostMapping public ResponseEntityTodo createTodo(AuthenticationPrincipal UserDetails userDetails, RequestBody Valid CreateTodoRequest request) { Long userId getUserIdFromPrincipal(userDetails); // 从安全上下文中提取用户ID Todo newTodo todoService.createTodo(userId, request); return ResponseEntity.status(HttpStatus.CREATED).body(newTodo); } GetMapping public ResponseEntityListTodo getMyTodos(AuthenticationPrincipal UserDetails userDetails) { Long userId getUserIdFromPrincipal(userDetails); return ResponseEntity.ok(todoService.getUserTodos(userId)); } PutMapping(/{id}) public ResponseEntityTodo updateTodo(AuthenticationPrincipal UserDetails userDetails, PathVariable Long id, RequestBody Valid UpdateTodoRequest request) { Long userId getUserIdFromPrincipal(userDetails); return ResponseEntity.ok(todoService.updateTodo(userId, id, request)); } DeleteMapping(/{id}) public ResponseEntityVoid deleteTodo(AuthenticationPrincipal UserDetails userDetails, PathVariable Long id) { Long userId getUserIdFromPrincipal(userDetails); todoService.deleteTodo(userId, id); return ResponseEntity.noContent().build(); } private Long getUserIdFromPrincipal(UserDetails userDetails) { // 假设UserDetails实现中包含了用户ID信息 // 这里是一个简化实现实际可能需要从数据库查询 return ((CustomUserDetails) userDetails).getId(); } }注意在真实的项目中getUserIdFromPrincipal的实现需要更严谨通常UserDetails只包含用户名和权限用户ID需要额外存储或查询。我这里为了快速演示做了一些简化。手搓代码时我非常清楚这里的妥协和待办事项会在后续迭代中完善。2.4 认证与安全配置这是最复杂的一部分。我配置了Spring Security使用JWT进行无状态认证。// SecurityConfig.java Configuration EnableWebSecurity RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; private final UserDetailsService userDetailsService; Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) // 对API通常禁用CSRF .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/**, /h2-console/**).permitAll() // 放行登录和H2控制台 .anyRequest().authenticated() // 其他所有请求需要认证 ) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态 .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) // 添加JWT过滤器 .userDetailsService(userDetailsService); // 允许嵌入H2控制台的帧仅用于开发 http.headers(headers - headers.frameOptions(frameOptions - frameOptions.sameOrigin())); return http.build(); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }同时我编写了JWT工具类和认证过滤器。这部分代码较为模板化但涉及密钥管理、令牌解析、异常处理等细节必须小心。// JwtService.java Component public class JwtService { Value(${jwt.secret}) private String secretKey; Value(${jwt.expiration}) private long jwtExpiration; public String generateToken(UserDetails userDetails) { MapString, Object claims new HashMap(); // 可以在这里添加自定义声明如用户ID、角色等 return Jwts.builder() .setClaims(claims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() jwtExpiration)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); } // ... 省略验证和解析Token的方法 } // JwtAuthenticationFilter.java Component RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; Override protected void doFilterInternal(NonNull HttpServletRequest request, NonNull HttpServletResponse response, NonNull FilterChain filterChain) throws ServletException, IOException { final String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { filterChain.doFilter(request, response); return; } final String jwt authHeader.substring(7); final String username jwtService.extractUsername(jwt); if (username ! null SecurityContextHolder.getContext().getAuthentication() null) { UserDetails userDetails this.userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }手搓一小时的成果与体会 在高度专注的一小时后我完成了一个具备基础CRUD、用户关联和JWT认证的待办事项API后端。代码结构清晰分层明确关键的安全校验用户数据隔离已经实现。整个过程行云流水因为我对每一行代码的意图、每一个依赖的作用都了如指掌。优势在于绝对的掌控感、对业务逻辑的深度嵌入如权限校验、以及对潜在坑点的预判如N1查询问题我可以通过EntityGraph注解在需要时优化。劣势也很明显进度完全依赖于我的打字速度和思维连续性编写大量样板代码如实体类字段、简单的Service方法消耗了不少时间而且整个代码风格和设计完全受我个人习惯影响。3. 第二回合借助“外脑”——AI编程的效率与惊喜时间来到第二个小时。我关闭了刚才的项目新建了一个目录打开了Cursor编辑器。我的目标不变但工作方式彻底改变我将从一个产品经理或架构师的视角用自然语言向AI描述需求让它生成代码我来做“代码审查员”和“集成工程师”。3.1 从零开始的“对话式”搭建我的第一条指令是“使用Spring Boot 3创建一个待办事项API后端项目需要包含User和Todo两个实体它们之间是多对一关系。使用Spring Data JPA和H2内存数据库。请给出主要的pom.xml依赖。”AICursor背后的模型几乎瞬间就给出了正确的依赖列表与我自己写的几乎一致但更贴心地注释了每个依赖的作用。接着我让它生成实体类。我输入“根据上面的依赖创建User和Todo的JPA实体类。User需要有id, username, password, email。Todo需要有id, title, description, completed, createdAt, 以及关联的User。使用Lombok注解。”AI生成的代码质量很高正确地使用了Entity,Data,ManyToOne等注解。但它生成的Todo实体中createdAt字段的类型是Date。我立刻纠正“请使用Java 8的LocalDateTime类型代替Date并设置createdAt在创建时自动设置为当前时间。”AI迅速修正了代码并添加了CreationTimestamp注解来自Hibernate的建议。我选择了更通用的方式告诉它“我们将在Service层手动设置createdAt请移除CreationTimestamp。” 这个过程像是一场精准的代码审查对话。3.2 业务逻辑与API的“需求描述”生成接下来是重头戏。我没有直接要Service和Controller而是描述场景“现在需要实现Todo的CRUD服务。核心要求是1. 每个用户只能操作自己的待办事项。2. 创建待办事项时需要关联当前登录用户。请先创建TodoService类包含创建、查询获取用户自己的所有待办、更新、删除方法。更新和删除时必须校验待办事项是否属于当前用户。”AI生成的TodoService核心逻辑与我手写的惊人相似同样包含了权限校验。但它生成的异常是RuntimeException。我要求更具体“请使用更具体的异常比如在资源不存在时抛出EntityNotFoundException在权限不足时抛出AccessDeniedException。” AI照做了。然后我让它生成REST控制器“基于上面的TodoService创建一个RESTful风格的TodoController。使用/api/todos作为根路径。需要处理POST创建、GET获取列表、PUT更新、DELETE删除。用户身份从Spring Security的AuthenticationPrincipal注入。”AI生成的Controller结构正确但它假设UserDetails直接包含了User实体或ID。我指出问题“AuthenticationPrincipal注入的UserDetails通常只包含用户名。如何获取用户ID请提供一个合理的解决方案。”AI给出了几个选项1. 在JWT中嵌入用户ID作为自定义声明。2. 在登录成功后将用户ID存入UserDetails的自定义实现中。3. 在每次请求时根据用户名从数据库查询用户ID。它分析了每种方案的优缺点。我选择了方案2并让它生成一个CustomUserDetails类。这次协作AI不仅生成了代码还提供了架构决策支持。3.3 安全与认证的“填空式”协作对于最复杂的JWT认证部分我采取了分步策略。我先说“现在配置Spring Security使用JWT进行无状态认证。放行/api/auth/**和/h2-console/**路径其他路径需要认证。请生成SecurityConfig配置类。”AI生成的配置类基本正确但默认开启了CSRF保护。我告诉它“对于纯API后端请禁用CSRF保护。” 它立刻修改了配置。接着我让它生成JWT工具类JwtService并指定了生成令牌和验证令牌的方法签名。AI完整地实现了这些方法包括使用io.jsonwebtoken库。我注意到它生成的密钥签名算法是HS256这是合适的。最后我要求创建JwtAuthenticationFilter。我描述了过滤器的流程“检查Authorization请求头提取JWT验证有效性如果有效则从数据库中加载UserDetails并设置到安全上下文中。” AI生成的过滤器代码逻辑清晰甚至包含了SecurityContextHolder的设置与我手写的版本在结构上大同小异。3.4 AI的“超预期”发挥与需要警惕的“幻觉”在基础功能完成后我尝试提出更复杂的需求“我想为待办事项添加一个简单的数据统计功能比如统计某个用户今天创建了多少待办项以及总完成率。请修改TodoRepository和TodoService来实现。”AI的反应令人印象深刻。它没有直接写代码而是先询问“统计‘今天’是基于createdAt字段的日期部分吗总完成率是指该用户所有待办事项中已完成的比例吗” 在得到我的肯定后它生成了如下查询方法// 在TodoRepository中添加 Query(SELECT COUNT(t) FROM Todo t WHERE t.user.id :userId AND DATE(t.createdAt) CURRENT_DATE) Long countTodayTodosByUserId(Param(userId) Long userId); Query(SELECT COUNT(t) FROM Todo t WHERE t.user.id :userId AND t.completed true) Long countCompletedTodosByUserId(Param(userId) Long userId); Query(SELECT COUNT(t) FROM Todo t WHERE t.user.id :userId) Long countAllTodosByUserId(Param(userId) Long userId);并在Service中组合这些方法计算出了完成率。它甚至考虑到了除零错误“如果总数为0则完成率为0。” 这种对边界条件的考虑显示了AI在理解上下文后具备一定的逻辑推理能力。然而AI并非万能。在另一个请求中我让它“生成一个初始化一些测试数据的数据库脚本data.sql”。它生成的脚本试图向todo表插入数据但外键user_id引用了不存在的用户ID导致应用启动时会报错。这就是AI的“幻觉”——它根据模式生成了看似合理的代码但忽略了数据间的逻辑一致性。这提醒我们AI生成的代码尤其是涉及数据操作和复杂逻辑的必须经过严格的人工审查和测试。AI协作一小时的成果与体会 在一小时内通过与AI的连续对话和指令修正我同样得到了一个功能基本完备的后端项目。优势极其明显1.速度描述需求比逐行敲代码快得多尤其是在生成样板代码和常见模式时。2.知识广度AI能快速提供不同实现方案的利弊充当了一个随时在线的资深顾问。3.代码一致性AI生成的代码风格统一遵循常见的实践规范。劣势同样突出1.上下文依赖需要清晰、无歧义的需求描述模糊的指令会导致错误结果。2.缺乏深层业务理解AI无法理解你业务中独有的、未明确表述的规则。3.“幻觉”风险可能生成语法正确但逻辑错误、或引用不存在的类库的代码。4.调试成本转移当AI生成的代码出错时定位问题可能比调试自己写的代码更耗时因为你并不完全清楚它的“思考”过程。4. 深度对比掌控感、思维过程与代码质量的较量经过两小时的实战我从几个维度对两种方式进行了深入比较。4.1 开发速度与初期效率单纯从“产出可运行代码”的速度来看AI编程在项目启动和搭建基础框架阶段具有压倒性优势。我可以用几句话就得到一个结构完整的项目骨架、实体类、仓库接口甚至基础的API。这节省了大量敲击键盘和回忆API用法的时间。然而“手搓代码”在复杂业务逻辑的首次实现上有时反而更快。因为当我自己在脑中梳理逻辑并转化为代码时是一个连续的、高度内聚的思维流。而使用AI我需要将连贯的逻辑拆解成多个精确的、原子化的指令并在生成后检查、调整、合并这个“沟通-反馈”循环有时会打断思维的流畅性。对于非常熟悉的技术栈我手写一个Service方法可能比用语言向AI描述清楚并验证结果更快。4.2 代码质量与可维护性代码结构两者在良好指令下都能产出结构清晰的代码。但AI更倾向于生成“标准答案”即社区最常见、最推荐的做法。而“手搓”的代码则带有强烈的个人风格和设计取舍。业务逻辑嵌入度这是“手搓”的绝对优势领域。在编写权限校验if (!todo.getUser().getId().equals(userId))这一行时我脑子里同步思考着用户会话管理、前端传递的令牌、以及整个安全链条。这个逻辑是“长”在业务上下文里的。AI虽然能根据指令生成这行代码但它并不真正“理解”这个校验为何如此重要以及它如何与其他安全模块如Spring Security的PreAuthorize协同。对于业务规则复杂、充满特殊情况和边界的领域“手搓”代码的深度和贴合度是AI目前难以企及的。错误处理与健壮性AI在明确指令下可以生成不错的异常处理。例如我要求“资源不存在时抛出EntityNotFoundException”它能做到。但它缺乏对异常处理策略一致性的全局观。比如在整个项目中是统一使用ControllerAdvice进行全局异常处理还是在某些地方就地处理AI生成的代码片段不会主动考虑这个架构级问题。而“手搓”时我通常会在项目初期就定下异常处理的基调。4.3 对开发者思维的影响这是最值得深思的一点。“手搓代码”是一个深度创造和设计的过程。从数据库表设计到API端点规划从事务边界划定到缓存策略考量每一步都伴随着主动的决策和权衡。这个过程极大地锻炼了系统设计能力和对技术栈的深度掌握。AI编程更像是一个“高级搜索”和“代码合成”的过程。我的角色从“建造者”部分转变为“架构描述者”和“代码审查者”。我的核心能力要求从“熟练背诵API”转向了“精准描述问题”、“甄别代码质量”和“进行系统集成”。它不会削弱编程能力但会转移能力重心。一个只会对着AI说“给我写个登录功能”的开发者如果看不懂AI生成的Spring Security配置和JWT过滤器那么当出现一个诡异的认证失败bug时他将束手无策。4.4 学习曲线与知识沉淀对于学习者“手搓代码”是无可替代的入门路径。通过亲手处理依赖冲突、调试空指针异常、理解ORM的懒加载与急加载获得的经验是刻骨铭心的。AI作为学习工具如果使用不当例如直接复制粘贴而不理解很容易导致“知识虚胖”——知道很多概念但无一精通。但对于有经验的开发者AI是一个强大的知识扩展和效率工具。当需要快速上手一个不熟悉的库例如用Spring AI接入大模型时让AI生成一个示例代码片段比自己阅读冗长的官方文档要高效得多。它可以帮你快速跨越“入门”的鸿沟。5. 结论不是取代而是进化——找到你的“人机共生”模式2个小时的对决没有绝对的胜者。这不是一场“你死我活”的竞赛而是一次揭示未来工作模式的探索。对于标准化、模式化的任务项目初始化、实体类生成、简单的CRUD、基础配置AI是毋庸置疑的效率倍增器。它能把开发者从重复劳动中解放出来。对于复杂的、非标准的业务逻辑、核心算法、系统架构设计以及调试深度bug人类开发者的经验、直觉和创造性思维仍然是核心。AI可以作为辅助提供参考实现或不同思路但决策权和最终责任在人。所以更厉害的不是“手搓代码”或“AI编程”本身而是能够驾驭AI的开发者。未来的优秀程序员很可能是一个“会编程的AI训练师”。他的工作流程可能是这样的架构与设计人类负责顶层设计拆解需求规划模块和接口。基础搭建用AI快速生成项目骨架、数据模型和样板代码。核心逻辑实现对于复杂业务部分人类亲自编写或与AI进行高强度对话不断细化、修正指令直至生成满意代码。审查、测试与集成人类严格审查AI生成的所有代码编写单元测试和集成测试将各个AI生成的模块与手写模块无缝集成。调试与优化当出现问题时人类利用对系统的深刻理解进行调试。AI可以辅助分析日志、提供可能的错误原因但最终定位和修复靠人。我个人在实际操作中的体会是将AI定位为一个“超级智能的代码补全和知识问答伙伴”是最舒服的状态。我不会期待它独立完成一个功能模块但我会频繁地向它提问“Spring Security中如何自定义AccessDeniedHandler”“帮我写一个根据经纬度计算距离的Haversine公式函数。”“我这段代码的复杂度有点高有没有重构的建议”这场2小时的实验让我确信恐惧AI会取代程序员是多余的但忽视AI带来的效率革命则是愚蠢的。真正的风险不在于AI本身而在于我们是否愿意进化自己的工作方式从代码的“打字员”转变为问题的“定义者”和方案的“架构师”。这场进化已经开始了。
返回列表