Java开发入门:从零开始构建第一个RESTAPI

Java开发入门:从零开始构建第一个RESTAPI
你打开IDE心里默念我要用Java写一个REST API。这个念头可能是来自产品经理的紧迫需求或是你想从CRUD工程师跃迁为全栈开发者的第一步。别被“REST”这个词吓到——它本质上就是一对规则告诉你的API如何用HTTP动词去拥抱资源。资源就是数据比如用户、订单、文章HTTP动词就是GET、POST、PUT、DELETE。Java生态里有Spring Boot这个黑魔法工具它帮你省掉配置Tomcat的麻烦让你把注意力聚焦在业务逻辑上。今天我们就从零开始不跳过任何细节构建一个能真正运行在浏览器或Postman里的REST API。项目骨架用Spring Initializr一键生成到start.spring.io去这是你的神殿。选择Maven Project、Java 17或21、Spring Boot 3.x。填入Groupcom.exampleArtifactdemo。依赖搜索框里至少要勾选Spring Web提供REST支持、Spring Boot DevTools热部署、Lombok减少样板代码。点Generate下载zip后解压用IntelliJ或VS Code打开。你会看到一个DemoApplication.java里面有个main方法。别碰它它是启动入口。你真正的战场在com.example.demo包下。你的第一个任务是让程序跑起来。右键运行DemoApplication控制台输出Spring的Banner并看到Tomcat started on port 8080。恭喜已经有一个空壳的Web应用在运行。现在访问http://localhost:8080会返回一个Whitelabel Error Page——正常因为你还没建立任何控制器。第一个端点用RestController宣示主权新建一个包controller在里面创建HelloController.java。粘贴如下代码package com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; RestController public class HelloController { GetMapping(/api/hello) public String sayHello() { return Hello, World!; } }关键点RestController告诉Spring这个类的所有方法返回值都直接写入HTTP响应体而不是跳转到视图模板。加上GetMapping(/api/hello)就定义了一个GET端点。重启应用访问http://localhost:8080/api/hello浏览器里会显示“Hello, World!”。你刚刚完成了零到一的突破。现在开始不要满足于字符串。REST API的核心是操作资源资源通常以JSON格式呈现。我们需要让API返回Java对象Spring会自动将其序列化为JSON。这是Spring Boot默认在classpath里包含Jackson依赖的功劳你不必手动配置任何json库。定义资源从简单POJO开始资源模型应该放在model包下。新建User.javapackage com.example.demo.model; import lombok.Data; Data public class User { private Long id; private String name; private String email; }Data是Lombok的注解自动生成getter、setter、toString、equals等。没有它你需要手写几十行代码。现在修改HelloController返回一个用户列表GetMapping(/api/users) public ListUser getUsers() { User user new User(); user.setId(1L); user.setName(Alice); user.setEmail(aliceexample.com); return Collections.singletonList(user); }访问/api/users你会看到JSON数组。看这就是REST的启蒙HTTP GET /api/users 返回用户集合资源通过URL路径标识数据通过JSON交换。但每次硬编码创建对象显然不靠谱。你需要一个数据层但本环节先不用数据库用内存里的静态列表模拟。CRUD的骨架GET、POST、PUT、DELETE新建service包创建UserService.java这个服务类负责管理一个ConcurrentHashMap作为持久化存储。同时让控制器注入这个服务。package com.example.demo.service; import com.example.demo.model.User; import org.springframework.stereotype.Service; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; Service public class UserService { private final MapLong, User store new ConcurrentHashMap(); private final AtomicLong idCounter new AtomicLong(1); public User createUser(User user) { long id idCounter.getAndIncrement(); user.setId(id); store.put(id, user); return user; } public User getUser(Long id) { return store.get(id); } public ListUser getAllUsers() { return new ArrayList(store.values()); } public User updateUser(Long id, User user) { if (!store.containsKey(id)) return null; user.setId(id); store.put(id, user); return user; } public boolean deleteUser(Long id) { return store.remove(id) ! null; } }然后控制器扩展为RestController RequestMapping(/api/users) public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } GetMapping public ListUser getAll() { return userService.getAllUsers(); } GetMapping(/{id}) public User getById(PathVariable Long id) { return userService.getUser(id); } PostMapping public User create(RequestBody User user) { return userService.createUser(user); } PutMapping(/{id}) public User update(PathVariable Long id, RequestBody User user) { return userService.updateUser(id, user); } DeleteMapping(/{id}) public void delete(PathVariable Long id) { userService.deleteUser(id); } }注意几个要点RequestMapping(/api/users)在类级别定义了根路径省去每个方法重复写路径。PathVariable绑定URL里的占位符RequestBody把请求体JSON反序列化为User对象。现在用Postman测试POST到/api/usersbody为{name:Bob,email:bobtest.com}返回201状态码Spring默认201因为PostMapping返回201 Created。GET /api/users/1返回那个用户。PUT /api/users/1修改email。DELETE /api/users/1删除。但你发现没有如果请求的ID不存在API返回了null或者状态码是200但body为空。这不是规范的REST响应。规范的做法是GET单个资源不存在时返回404POST创建返回201PUT更新返回200或204DELETE成功返回204。我们需要引入HTTP状态码的显式控制。让错误更优雅ResponseEntity与全局异常处理修改控制器的getById方法GetMapping(/{id}) public ResponseEntityUser getById(PathVariable Long id) { User user userService.getUser(id); if (user null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(user); }delete方法也改一下DeleteMapping(/{id}) public ResponseEntityVoid delete(PathVariable Long id) { boolean deleted userService.deleteUser(id); if (!deleted) { return ResponseEntity.notFound().build(); } return ResponseEntity.noContent().build(); }但每个方法都写if判断会很笨重。更好的方式是使用全局异常处理。自定义一个ResourceNotFoundException然后加上ControllerAdvice。ResponseStatus(HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } }在getById中直接return userService.getUser(id).orElseThrow(() - new ResourceNotFoundException(User not found));但因为我们之前用null返回现改为Optional。重新设计service的getUser返回Optional 。然后在controller中GetMapping(/{id}) public User getById(PathVariable Long id) { return userService.getUser(id) .orElseThrow(() - new ResourceNotFoundException(User with id id not found)); }然后创建GlobalExceptionHandlerRestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(ResourceNotFoundException.class) ResponseStatus(HttpStatus.NOT_FOUND) public MapString, String handleNotFound(ResourceNotFoundException ex) { return Map.of(error, ex.getMessage()); } ExceptionHandler(MethodArgumentNotValidException.class) ResponseStatus(HttpStatus.BAD_REQUEST) public MapString, String handleValidation(MethodArgumentNotValidException ex) { // 提取字段验证错误 String message ex.getBindingResult().getAllErrors().stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.joining(, )); return Map.of(error, message); } }全局异常处理让你把错误逻辑从控制器中剥离REST API变得整洁且一致。每个异常类型对应一个HTTP状态码和统一格式的错误响应体。这是专业API的标配。数据验证用Jakarta Validation保驾护航用户提交的数据必须校验。加入spring-boot-starter-validation旧版本需要手动加Spring Boot 3.x已经包含。在User类字段上加注解Data public class User { private Long id; NotBlank(message Name cannot be blank) private String name; Email(message Email must be valid) NotBlank(message Email cannot be blank) private String email; }然后在控制器中的RequestBody前加ValidPostMapping public ResponseEntityUser create(Valid RequestBody User user) { User created userService.createUser(user); return ResponseEntity.status(HttpStatus.CREATED).body(created); }当你发送空name或非法email时Spring会自动抛出MethodArgumentNotValidException被我们的全局异常处理器捕获返回400和错误信息。不要信任任何客户端输入后端校验是安全的最后一道防线。让API可发现HATEOAS不先做好基础分页和过滤HATEOAS是REST成熟度模型第三级但多数生产API只用到第二级资源加动词。更实际的是你API需要支持分页、排序、按字段过滤。Spring Data提供了Pageable接口但你目前没有数据库。我们可以模拟分页在service中根据参数返回子列表。但更佳实践是直接引入Spring Data JPA并连接一个H2内存数据库让数据持久化天然支持分页。这是很多教程跳过的步骤但我们不跳。在pom.xml里添加dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency删除之前的UserService和内存存储创建User实体和JpaRepositoryEntity Table(name users) Data public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; NotBlank private String name; Email NotBlank private String email; }UserRepositorypublic interface UserRepository extends JpaRepositoryUser, Long { ListUser findByNameContainingIgnoreCase(String name); }控制器修改为直接注入Repository但为了分层还是创建serviceService Transactional public class UserService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository repository; } public User createUser(User user) { return repository.save(user); } public OptionalUser getUser(Long id) { return repository.findById(id); } public PageUser getAllUsers(Pageable pageable) { return repository.findAll(pageable); } public OptionalUser updateUser(Long id, User newData) { return repository.findById(id).map(existing - { existing.setName(newData.getName()); existing.setEmail(newData.getEmail()); return repository.save(existing); }); } public boolean deleteUser(Long id) { if (repository.existsById(id)) { repository.deleteById(id); return true; } return false; } }控制器getAll接收Pageable参数GetMapping public PageUser getAll( RequestParam(defaultValue 0) int page, RequestParam(defaultValue 10) int size, RequestParam(defaultValue id) String sortBy, RequestParam(defaultValue asc) String sortDir) { Sort sort sortDir.equalsIgnoreCase(asc) ? Sort.by(sortBy).ascending() : Sort.by(sortBy).descending(); Pageable pageable PageRequest.of(page, size, sort); return userService.getAllUsers(pageable); }当你请求GET /api/users?page0size5sortBynamesortDirdescSpring Data JPA自动生成SQL查询并返回分页后的JSON包含totalPages、totalElements、content等元数据。分页是任何CURD API的必修课直接返回整个表是反模式。测试你的API不只是手工Postman集成测试用Spring Boot的WebMvcTest。新建test类WebMvcTest(UserController.class) class UserControllerTest { Autowired private MockMvc mockMvc; MockBean private UserService userService; Test void shouldReturnUserWhenExists() throws Exception { User user new User(); user.setId(1L); user.setName(Test); user.setEmail(testexample.com); given(userService.getUser(1L)).willReturn(Optional.of(user)); mockMvc.perform(get(/api/users/1)) .andExpect(status().isOk()) .andExpect(jsonPath($.name).value(Test)); } Test void shouldReturn404WhenNotFound() throws Exception { given(userService.getUser(99L)).willReturn(Optional.empty()); mockMvc.perform(get(/api/users/99)) .andExpect(status().isNotFound()); } }自动化测试能让你在重构时立即发现破坏性变更。别偷懒每一层都写点测试。除了Web层还应写service层单元测试使用ExtendWith(MockitoExtension.class)和Repository层测试DataJpaTest。安全第一位添加简单的API Key验证虽然完整认证要用Spring Security JWT但入门阶段可以尝一个简单的filter来验证请求头中的API Key。实现一个OncePerRequestFilterComponent public class ApiKeyFilter extends OncePerRequestFilter { private static final String API_KEY my-secret-key-123; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String apiKey request.getHeader(X-API-KEY); if (apiKey null || !apiKey.equals(API_KEY)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write({\error\:\Invalid API key\}); return; } filterChain.doFilter(request, response); } }然后配置FilterRegistrationBean在控制器之前执行Bean public FilterRegistrationBeanApiKeyFilter apiKeyFilterRegistration(ApiKeyFilter filter) { FilterRegistrationBeanApiKeyFilter registration new FilterRegistrationBean(); registration.setFilter(filter); registration.addUrlPatterns(/api/); registration.setOrder(1); return registration; }现在你的API受到硬编码密钥保护。当然生产环境绝不会这么干但理解过滤器机制是学习Spring Security的前奏。文档自动生成Swagger/OpenAPI配合springdoc-openapi-starter-webmvc-uiSpring Boot 3版本在pom.xml添加dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.3.0/version /dependency启动后访问http://localhost:8080/swagger-ui.html你会看到交互式API文档。你可以为控制器和方法添加Operation和ApiResponse注解描述业务含义。好的API文档不止是曲线救赎更是团队协作的基础设施。进阶输入验证、自定义序列化、多版本共存你还可以做很多使用JsonView控制序列化字段暴露通过ControllerAdvice统一包装响应格式例如总是返回{status, message, data}结构利用Spring的Content Negotiation支持XML或自定义格式使用版本控制路径版/v1/users头部版Accept: application/vnd.myapp.v2json。记住REST API的终极奥义是使客户端与服务端解耦。每个端点应当幂等GET、PUT、DELETE符合POST不一定URL应该命名复数名词使用复数而不是单数/users而不是/user不要出现动词/getUsers这种错误。HTTP状态码是沟通语言务必正确使用201表示创建成功204表示无内容400表示客户端错误401表示未认证403表示未授权404表示资源不存在500表示服务器内部错误。把项目打包成可交付制品在pom.xml里配置Spring Boot Maven插件然后用命令行mvn clean package生成一个fat jar文件。在服务器上用java -jar target/demo-0.0.1-SNAPSHOT.jar运行你的REST API就能在任意环境启动。你还可以在application.properties里设置server.port80以及数据库连接、日志级别等。回过头看看你从Hello World走到了一个分页、校验、异常处理、认证、文档齐全的REST API。这个过程不是背语法而是建立一种思维模型资源经过URL暴露操作通过HTTP动词映射数据通过JSON流动错误通过状态码和格式传递。你的第一个API可能只有几百行代码但它承载着REST全貌。将来面对更复杂的微服务、事件驱动架构时今天打下的地基永远不会浪费。现在去构建更多接口吧。把这段代码提交到GitHub叫它“first-rest-api”然后告诉世界你跨过了Java Web开发的门槛。