基于Java的RESTful汽车服务实现Conditional-GET功能问询
Hey Tommy, great question! Adding Conditional-GET support is a fantastic way to optimize your API—cutting down on unnecessary data transfer and lightening server load. Let’s walk through exactly how to implement this using the Last-Modified/If-Modified-Since headers you mentioned, plus a bonus tip on ETag for even better precision.
Core Concept Recap
Conditional-GET works like this:
- When a client first requests a car resource, your server returns the data and a
Last-Modifiedheader containing the timestamp of when the resource was last updated. - On subsequent requests, the client sends an
If-Modified-Sinceheader with that same timestamp. - Your server compares the resource’s current last-modified time to the value in
If-Modified-Since. If the resource hasn’t changed, return304 Not Modified(no body needed); if it has, return200 OKwith the updated data and freshLast-Modifiedheader.
Step-by-Step Implementation (Spring Boot Example)
Assuming you’re using Spring Boot (the most common framework for Java REST services), here’s how to modify your existing GET endpoint:
1. Update Your Entity to Track Last-Modified Time
First, make sure your Car entity has a field to track when it was last updated (most databases support auto-updating this on save):
@Entity public class Car { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String make; private String model; // ... other fields @Column(name = "updated_at") private LocalDateTime updatedAt; // Auto-update timestamp on save/update @PreUpdate @PrePersist public void updateTimestamp() { this.updatedAt = LocalDateTime.now(); } // Getters and setters }
2. Modify the GET Endpoint to Handle Conditional Logic
Update your controller method to check the If-Modified-Since header and return the appropriate status:
@GetMapping("/cars/{id}") public ResponseEntity<Car> getCar( @PathVariable Long id, @RequestHeader(value = "If-Modified-Since", required = false) String ifModifiedSince ) { // Fetch the car from your database Car car = carRepository.findById(id) .orElseThrow(() -> new RuntimeException("Car not found with ID: " + id)); // Convert entity's updated time to epoch milliseconds ZonedDateTime lastModifiedZoned = car.getUpdatedAt().atZone(ZoneId.systemDefault()); long lastModifiedMillis = lastModifiedZoned.toInstant().toEpochMilli(); // Check if client sent an If-Modified-Since header if (ifModifiedSince != null) { try { // Parse the HTTP-standard RFC 1123 date format SimpleDateFormat httpDateFormat = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US ); long clientTimestamp = httpDateFormat.parse(ifModifiedSince).getTime(); // If resource hasn't changed, return 304 if (lastModifiedMillis <= clientTimestamp) { return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); } } catch (ParseException e) { // Malformed header? Ignore it and return 200 as usual e.printStackTrace(); } } // Return 200 OK with data and Last-Modified header return ResponseEntity.ok() .lastModified(lastModifiedMillis) .body(car); }
3. Bonus: Add ETag for Higher Precision
Last-Modified only tracks time to the second, which might not be enough if your resource can be updated multiple times per second. For better accuracy, add an ETag (a unique hash of the resource content) alongside Last-Modified:
@GetMapping("/cars/{id}") public ResponseEntity<Car> getCar( @PathVariable Long id, @RequestHeader(value = "If-Modified-Since", required = false) String ifModifiedSince, @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch ) { Car car = carRepository.findById(id) .orElseThrow(() -> new RuntimeException("Car not found with ID: " + id)); ZonedDateTime lastModifiedZoned = car.getUpdatedAt().atZone(ZoneId.systemDefault()); long lastModifiedMillis = lastModifiedZoned.toInstant().toEpochMilli(); // Generate ETag using car ID + updated timestamp (could also hash the full entity) String eTag = "\"" + car.getId() + "-" + lastModifiedMillis + "\""; // Check ETag first (takes priority over Last-Modified per HTTP specs) if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) { return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); } // Then check If-Modified-Since as before if (ifModifiedSince != null) { try { SimpleDateFormat httpDateFormat = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US ); long clientTimestamp = httpDateFormat.parse(ifModifiedSince).getTime(); if (lastModifiedMillis <= clientTimestamp) { return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); } } catch (ParseException e) { e.printStackTrace(); } } // Return 200 OK with both headers return ResponseEntity.ok() .lastModified(lastModifiedMillis) .eTag(eTag) .body(car); }
Testing with Postman
- First Request: Send a GET to
/cars/{id}. Check the response headers—you’ll seeLast-Modified(andETagif you added it). - Subsequent Request: Add the
If-Modified-Sinceheader (using the value from the first request’sLast-Modified) orIf-None-Match(using theETagvalue) to your request. If the car data hasn’t changed, you’ll get a304 Not Modifiedresponse with no body.
Key Notes
- Always update the
updatedAtfield whenever you perform a PUT/PATCH on a car (the@PreUpdatecallback handles this automatically). - The HTTP date format must be RFC 1123 (e.g.,
Wed, 21 Oct 2015 07:28:00 GMT)—Spring’slastModified()method converts milliseconds to this format for you. - A
304response should never include a body—useResponseEntity.status(HttpStatus.NOT_MODIFIED).build()to ensure this.
内容的提问来源于stack exchange,提问作者Tommy Hamm

