JAX-RS とは?基礎から最新動向まで完全解説
JAX-RS の概要と主要な特徴
JAX-RS(Jakarta RESTful Web Services、旧Java API for RESTful Web Services)は、JavaでRESTful Webサービスを構築するための標準仕様です。アノテーションベースの直感的なAPIを提供し、REST原則に基づいたWebサービスの開発を容易にします。
主要な特徴:
- 宣言的なアノテーション
@Path: リソースのパスを指定@GET,@POST,@PUT,@DELETE: HTTPメソッドを定義@Produces,@Consumes: メディアタイプを指定@PathParam,@QueryParam: パラメータを取得
- 柔軟なメディアタイプサポート
- JSON, XML, テキスト等の多様な形式に対応
- カスタムメディアタイプの定義が可能
- Content Negotiationによる動的な形式選択
- フィルタとインターセプタ
- リクエスト/レスポンスの前後処理が可能
- クロスカッティングコンサーンの実装
- セキュリティ、ロギング等の機能追加
- 非同期処理のサポート
@Suspendedアノテーションによる非同期処理- Server-Sent Eventsのサポート
- WebSocketとの統合
Jakarta EE における JAX-RS の位置づけ
Jakarta EEエコシステムにおいて、JAX-RSは重要なコンポーネントとして位置づけられています:
- Jakarta EE 9での変更点
- パッケージ名が
javax.ws.rsからjakarta.ws.rsに変更 - より柔軟なDIサポート
- マイクロサービス指向の強化
- 他のJakarta EE技術との統合
@Path("/users")
@RequestScoped // CDIスコープ
public class UserResource {
@Inject
private UserService userService; // CDIによる依存性注入
@PersistenceContext
private EntityManager em; // JPAとの統合
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<User> getUsers() {
return userService.findAll();
}
}
- 実装ベンダー
- Eclipse Jersey(リファレンス実装)
- RESTEasy(Red Hat)
- Apache CXF
- それぞれの特徴と利点あり
従来のサーバーサイド実装との比較
従来のサーブレットベースの実装とJAX-RSを比較してみましょう:
- 従来のサーブレット実装
@WebServlet("/users/*")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String pathInfo = req.getPathInfo();
if (pathInfo == null || pathInfo.equals("/")) {
// 全ユーザーの取得
List<User> users = userService.findAll();
String json = new ObjectMapper().writeValueAsString(users);
resp.setContentType("application/json");
resp.getWriter().write(json);
} else {
// 特定ユーザーの取得
Long id = Long.parseLong(pathInfo.substring(1));
User user = userService.findById(id);
if (user != null) {
String json = new ObjectMapper().writeValueAsString(user);
resp.setContentType("application/json");
resp.getWriter().write(json);
} else {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
}
- JAX-RSによる実装
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
@Inject
private UserService userService;
@GET
public List<User> getAllUsers() {
return userService.findAll();
}
@GET
@Path("/{id}")
public Response getUserById(@PathParam("id") Long id) {
return userService.findById(id)
.map(user -> Response.ok(user).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
}
主な利点:
- 宣言的なAPI設計
- アノテーションによる直感的な実装
- コードの可読性と保守性の向上
- ボイラープレートコードの削減
- 標準化されたアプローチ
- RESTful原則に基づいた一貫した設計
- さまざまな実装ベンダー間での互換性
- 豊富なエコシステムとツールのサポート
- 柔軟性と拡張性
- フィルタやインターセプタによる機能拡張
- カスタムプロバイダーの実装が容易
- 様々なメディアタイプへの対応
このように、JAX-RSは従来の実装方法と比較して、より効率的でメンテナンス性の高いRESTful Webサービスの開発を可能にします。
JAX-RSによるRESTful API開発の基本
環境構築とプロジェクトセットアップ手順
JAX-RSを使用したRESTful API開発を始めるための環境構築とプロジェクトセットアップを説明します。
- 必要な依存関係
Maven projectの場合、pom.xmlに以下の依存関係を追加します:
<dependencies>
<!-- JAX-RS API -->
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Jersey(実装) -->
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>3.1.1</version>
</dependency>
<!-- Jersey DI support -->
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>3.1.1</version>
</dependency>
<!-- JSON support -->
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>
- アプリケーション設定
JAX-RSアプリケーションの基本設定:
@ApplicationPath("/api")
public class RestApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> resources = new HashSet<>();
// リソースクラスの登録
resources.add(UserResource.class);
return resources;
}
}
基本的な CRUD オペレーションの実装方法
ユーザー管理APIを例に、基本的なCRUD操作を実装します:
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
private final UserService userService;
@Inject
public UserResource(UserService userService) {
this.userService = userService;
}
// Create
@POST
public Response createUser(User user) {
User created = userService.create(user);
return Response
.status(Response.Status.CREATED)
.entity(created)
.build();
}
// Read
@GET
public List<User> getAllUsers() {
return userService.findAll();
}
@GET
@Path("/{id}")
public Response getUserById(@PathParam("id") Long id) {
return userService.findById(id)
.map(user -> Response.ok(user).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
// Update
@PUT
@Path("/{id}")
public Response updateUser(@PathParam("id") Long id, User user) {
if (!id.equals(user.getId())) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
User updated = userService.update(user);
return Response.ok(updated).build();
}
// Delete
@DELETE
@Path("/{id}")
public Response deleteUser(@PathParam("id") Long id) {
userService.delete(id);
return Response.noContent().build();
}
}
パスとパラメータの仕組み
JAX-RSでは、様々な方法でパラメータを受け取ることができます:
- パスパラメータ
@Path("/users/{id}/posts/{postId}")
public Response getUserPost(
@PathParam("id") Long userId,
@PathParam("postId") Long postId
) {
// パスから取得したパラメータを使用
return Response.ok(postService.findUserPost(userId, postId)).build();
}
- クエリパラメータ
@GET
@Path("/search")
public List<User> searchUsers(
@QueryParam("name") String name,
@QueryParam("age") Integer age,
@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("size") int size
) {
return userService.search(name, age, page, size);
}
- マトリックスパラメータ
@GET
@Path("/filter")
public List<User> filterUsers(
@MatrixParam("country") String country,
@MatrixParam("city") String city
) {
return userService.filter(country, city);
}
- フォームパラメータ
@POST
@Path("/login")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response login(
@FormParam("username") String username,
@FormParam("password") String password
) {
// フォームデータの処理
return authService.login(username, password);
}
- ヘッダーパラメータ
@GET
@Path("/secure")
public Response getSecureResource(
@HeaderParam("Authorization") String authHeader
) {
// 認証ヘッダーの処理
if (!authService.validateToken(authHeader)) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
return Response.ok(secureResource).build();
}
実装のポイント:
- 適切なHTTPメソッドの使用
- GET: リソースの取得
- POST: 新規リソースの作成
- PUT: リソースの更新
- DELETE: リソースの削除
- レスポンスの適切な設定
- ステータスコード
- レスポンスエンティティ
- ヘッダー情報
- パラメータバリデーション
- Bean Validationの活用
- カスタムバリデーションの実装
これらの基本を押さえることで、堅牢なRESTful APIの開発が可能になります。
実践的な JAX-RS 実装テクニック
効率的なリソースクラスの設計方法
効率的なリソースクラスを設計するためのベストプラクティスを解説します。
- レイヤー分離の実装
@Path("/orders")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class OrderResource {
private final OrderService orderService;
private final OrderMapper orderMapper;
@Inject
public OrderResource(OrderService orderService, OrderMapper orderMapper) {
this.orderService = orderService;
this.orderMapper = orderMapper;
}
@POST
public Response createOrder(OrderDTO orderDTO) {
// DTOからドメインモデルへの変換
Order order = orderMapper.toEntity(orderDTO);
// ビジネスロジックの実行
Order created = orderService.createOrder(order);
// レスポンスDTOの作成
OrderDTO response = orderMapper.toDTO(created);
return Response.status(Response.Status.CREATED)
.entity(response)
.build();
}
}
- サブリソースの活用
@Path("/departments")
public class DepartmentResource {
@Path("/{deptId}/employees")
public EmployeeSubResource getEmployeeResource(@PathParam("deptId") Long deptId) {
return new EmployeeSubResource(deptId);
}
}
public class EmployeeSubResource {
private final Long departmentId;
public EmployeeSubResource(Long departmentId) {
this.departmentId = departmentId;
}
@GET
public List<Employee> getEmployees() {
return employeeService.findByDepartment(departmentId);
}
@POST
public Response addEmployee(Employee employee) {
employee.setDepartmentId(departmentId);
return Response.ok(employeeService.create(employee)).build();
}
}
例外処理とエラー応答の実装
- カスタム例外クラス
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
- グローバル例外ハンドラー
@Provider
public class GlobalExceptionHandler implements ExceptionMapper<Throwable> {
@Override
public Response toResponse(Throwable exception) {
if (exception instanceof BusinessException) {
BusinessException be = (BusinessException) exception;
ErrorResponse error = new ErrorResponse(
be.getErrorCode().getCode(),
be.getMessage()
);
return Response.status(Response.Status.BAD_REQUEST)
.entity(error)
.build();
}
if (exception instanceof ConstraintViolationException) {
ConstraintViolationException cve = (ConstraintViolationException) exception;
List<String> violations = cve.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ValidationErrorResponse(violations))
.build();
}
// その他の例外はサーバーエラーとして処理
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("INTERNAL_ERROR", "内部エラーが発生しました"))
.build();
}
}
バリデーション機能の活用術
- Bean Validationの利用
public class UserDTO {
@NotNull(message = "名前は必須です")
@Size(min = 2, max = 100, message = "名前は2文字以上100文字以内で入力してください")
private String name;
@Email(message = "有効なメールアドレスを入力してください")
private String email;
@Min(value = 0, message = "年齢は0以上の値を入力してください")
@Max(value = 150, message = "年齢は150以下の値を入力してください")
private Integer age;
// getter/setter
}
@Path("/users")
public class UserResource {
@POST
public Response createUser(@Valid UserDTO userDTO) {
// バリデーション済みのDTOを使用
return Response.ok(userService.create(userDTO)).build();
}
}
- カスタムバリデーション
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "このメールアドレスは既に使用されています";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Inject
private UserService userService;
@Override
public boolean isValid(String email, ConstraintValidatorContext context) {
if (email == null) {
return true;
}
return !userService.existsByEmail(email);
}
}
public class UserDTO {
@UniqueEmail
private String email;
// 他のフィールド
}
- グループバリデーション
public interface CreateValidation {}
public interface UpdateValidation {}
public class ProductDTO {
@Null(groups = CreateValidation.class)
@NotNull(groups = UpdateValidation.class)
private Long id;
@NotNull(groups = {CreateValidation.class, UpdateValidation.class})
@Size(min = 3, max = 100)
private String name;
@NotNull(groups = CreateValidation.class)
@Positive
private BigDecimal price;
}
@Path("/products")
public class ProductResource {
@POST
public Response createProduct(
@Valid @Validated(CreateValidation.class) ProductDTO product
) {
return Response.ok(productService.create(product)).build();
}
@PUT
@Path("/{id}")
public Response updateProduct(
@PathParam("id") Long id,
@Valid @Validated(UpdateValidation.class) ProductDTO product
) {
return Response.ok(productService.update(id, product)).build();
}
}
これらの実装テクニックを活用することで、保守性が高く、堅牢なAPIを構築することができます。各機能を適材適所で使用し、アプリケーションの要件に合わせて適切に組み合わせることが重要です。
JAX-RSベストプラクティス9選
適切なHTTPメソッドとステータスコードの使用
- HTTPメソッドの適切な選択
@Path("/articles")
public class ArticleResource {
// 冪等性のある操作にはGETを使用
@GET
@Path("/{id}")
public Response getArticle(@PathParam("id") Long id) {
return articleService.findById(id)
.map(Response::ok)
.orElse(Response.status(Response.Status.NOT_FOUND))
.build();
}
// リソース作成にはPOSTを使用
@POST
public Response createArticle(ArticleDTO article) {
Article created = articleService.create(article);
return Response.status(Response.Status.CREATED)
.entity(created)
.location(URI.create("/articles/" + created.getId()))
.build();
}
// 完全な更新にはPUTを使用
@PUT
@Path("/{id}")
public Response updateArticle(@PathParam("id") Long id, ArticleDTO article) {
Article updated = articleService.update(id, article);
return Response.ok(updated).build();
}
// 部分更新にはPATCHを使用
@PATCH
@Path("/{id}")
public Response patchArticle(@PathParam("id") Long id, JsonPatch patch) {
Article patched = articleService.patch(id, patch);
return Response.ok(patched).build();
}
}
- 適切なステータスコードの使用
@Provider
public class ResponseStatusHandler implements ResponseStatusMapper {
public Response toResponse(Status status) {
switch (status) {
case SUCCESS:
return Response.status(Response.Status.OK).build();
case CREATED:
return Response.status(Response.Status.CREATED).build();
case NOT_FOUND:
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Resource not found"))
.build();
case VALIDATION_ERROR:
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Validation failed"))
.build();
default:
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Internal server error"))
.build();
}
}
}
セキュリティ対策の実装方法
- 認証フィルターの実装
@Provider
@Priority(Priorities.AUTHENTICATION)
public class JWTAuthenticationFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new NotAuthorizedException("認証トークンが必要です");
}
String token = authHeader.substring("Bearer ".length());
try {
// トークンの検証
Claims claims = validateToken(token);
// セキュリティコンテキストの設定
SecurityContext securityContext = new JWTSecurityContext(claims);
requestContext.setSecurityContext(securityContext);
} catch (Exception e) {
throw new NotAuthorizedException("無効なトークンです");
}
}
}
- CORS設定の実装
@Provider
public class CORSFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
responseContext.getHeaders().add("Access-Control-Allow-Origin", "https://example.com");
responseContext.getHeaders().add("Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS");
responseContext.getHeaders().add("Access-Control-Allow-Headers",
"Content-Type, Authorization");
responseContext.getHeaders().add("Access-Control-Max-Age", "86400");
}
}
- 入力のサニタイズ
public class InputSanitizer {
public static String sanitize(String input) {
if (input == null) {
return null;
}
// XSSを防ぐための基本的なサニタイズ
return input.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """)
.replaceAll("'", "'")
.replaceAll("&", "&");
}
}
パフォーマンス最適化のポイント
- キャッシュの実装
@Path("/cached-resources")
public class CachedResource {
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response getCachedResource(@PathParam("id") Long id) {
CachedEntity entity = resourceService.getById(id);
// ETagの生成
EntityTag etag = new EntityTag(String.valueOf(entity.hashCode()));
// 条件付きGETのチェック
Response.ResponseBuilder rb = request.evaluatePreconditions(etag);
if (rb != null) {
return rb.build();
}
// キャッシュヘッダーの設定
return Response.ok(entity)
.tag(etag)
.cacheControl(CacheControl.valueOf("max-age=3600"))
.build();
}
}
- ページネーションの実装
@Path("/items")
public class ItemResource {
@GET
public Response getItems(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size,
@QueryParam("sort") @DefaultValue("id,desc") String sort
) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sort.split(",")));
Page<Item> items = itemService.findAll(pageable);
return Response.ok(items)
.header("X-Total-Count", items.getTotalElements())
.header("X-Total-Pages", items.getTotalPages())
.build();
}
}
- 非同期処理の活用
@Path("/async")
public class AsyncResource {
@GET
@Path("/process")
public void asyncProcess(
@Suspended final AsyncResponse asyncResponse
) {
CompletableFuture.supplyAsync(() -> {
// 時間のかかる処理
return heavyProcess();
}).thenAccept(result -> {
asyncResponse.resume(Response.ok(result).build());
}).exceptionally(throwable -> {
asyncResponse.resume(Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(throwable.getMessage()))
.build());
return null;
});
}
}
- バッチ処理の最適化
@Path("/batch")
public class BatchResource {
@POST
@Path("/operations")
public Response batchOperations(List<Operation> operations) {
List<CompletableFuture<OperationResult>> futures = operations.stream()
.map(operation -> CompletableFuture.supplyAsync(() ->
processOperation(operation)))
.collect(Collectors.toList());
// 全ての処理が完了するのを待つ
List<OperationResult> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
return Response.ok(new BatchResponse(results)).build();
}
private OperationResult processOperation(Operation operation) {
try {
return operationService.process(operation);
} catch (Exception e) {
return new OperationResult(operation.getId(), Status.FAILED, e.getMessage());
}
}
}
これらのベストプラクティスを適切に組み合わせることで、セキュアで高性能なRESTful APIを構築することができます。実装の際は、アプリケーションの要件や制約に応じて、これらのプラクティスを適切にカスタマイズすることが重要です。
実践的なユースケースと実装例
ファイルアップロード機能の実装
- シングルファイルアップロード
@Path("/files")
public class FileUploadResource {
@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(
@FormDataParam("file") InputStream fileInputStream,
@FormDataParam("file") FormDataContentDisposition fileMetaData
) {
try {
// ファイル名の取得と安全性チェック
String fileName = fileMetaData.getFileName();
if (!isValidFileName(fileName)) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Invalid file name")
.build();
}
// ファイルの保存
String savedPath = saveFile(fileInputStream, fileName);
// メタデータの保存
FileMetadata metadata = new FileMetadata();
metadata.setFileName(fileName);
metadata.setPath(savedPath);
metadata.setUploadDate(LocalDateTime.now());
fileMetadataRepository.save(metadata);
return Response.status(Response.Status.CREATED)
.entity(metadata)
.build();
} catch (IOException e) {
return Response.serverError()
.entity("Failed to upload file")
.build();
}
}
private String saveFile(InputStream inputStream, String fileName) throws IOException {
String uploadDir = System.getProperty("user.home") + "/uploads/";
Files.createDirectories(Paths.get(uploadDir));
String uniqueFileName = UUID.randomUUID().toString() + "_" + fileName;
Path filePath = Paths.get(uploadDir, uniqueFileName);
Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING);
return filePath.toString();
}
}
- マルチファイルアップロード
@POST
@Path("/upload/multiple")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadMultipleFiles(
MultivaluedMap<String, FormDataBodyPart> formData
) {
List<FileMetadata> uploadedFiles = new ArrayList<>();
for (Map.Entry<String, List<FormDataBodyPart>> entry : formData.entrySet()) {
for (FormDataBodyPart bodyPart : entry.getValue()) {
if (bodyPart.getContentDisposition().getType().equals("attachment")) {
try {
InputStream fileStream = bodyPart.getValueAs(InputStream.class);
String fileName = bodyPart.getContentDisposition().getFileName();
String savedPath = saveFile(fileStream, fileName);
FileMetadata metadata = createFileMetadata(fileName, savedPath);
uploadedFiles.add(metadata);
} catch (IOException e) {
// エラーログの記録
continue;
}
}
}
}
return Response.status(Response.Status.CREATED)
.entity(uploadedFiles)
.build();
}
非同期処理の実装方法
- 非同期API処理
@Path("/async")
public class AsyncResource {
@Inject
private ExecutorService executorService;
@GET
@Path("/long-running")
public void longRunningOperation(@Suspended final AsyncResponse asyncResponse) {
// タイムアウトの設定
asyncResponse.setTimeout(30, TimeUnit.SECONDS);
asyncResponse.setTimeoutHandler(ar ->
ar.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
.entity("Operation timed out")
.build())
);
executorService.submit(() -> {
try {
// 時間のかかる処理
Result result = performLongRunningTask();
asyncResponse.resume(Response.ok(result).build());
} catch (Exception e) {
asyncResponse.resume(Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(e.getMessage())
.build());
}
});
}
}
- Server-Sent Events (SSE)の実装
@Path("/sse")
public class SSEResource {
@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
public void subscribeToEvents(@Context SseEventSink eventSink, @Context Sse sse) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try (SseEventSink sink = eventSink) {
for (int i = 0; i < 10; i++) {
OutboundSseEvent event = sse.newEventBuilder()
.id(String.valueOf(i))
.data(String.class, "Event " + i)
.build();
sink.send(event);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
// エラー処理
}
});
executor.shutdown();
}
}
キャッシュ制御の実践テクニック
- ETags と条件付きリクエストの実装
@Path("/cached")
public class CachedResource {
@GET
@Path("/{id}")
public Response getResource(
@PathParam("id") Long id,
@Context Request request
) {
Resource resource = resourceService.findById(id);
if (resource == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
// ETAGの生成
EntityTag etag = new EntityTag(Integer.toString(resource.hashCode()));
// 条件付きGETのチェック
Response.ResponseBuilder rb = request.evaluatePreconditions(etag);
if (rb != null) {
return rb.build();
}
// キャッシュヘッダーの設定
CacheControl cc = new CacheControl();
cc.setMaxAge(3600); // 1時間
cc.setPrivate(true);
return Response.ok(resource)
.tag(etag)
.cacheControl(cc)
.build();
}
}
- 動的なキャッシュ制御
@Provider
public class DynamicCacheControlFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
String path = requestContext.getUriInfo().getPath();
CacheControl cc = new CacheControl();
// パスに基づいてキャッシュ戦略を決定
if (path.startsWith("/api/public")) {
// 公開APIは長めにキャッシュ
cc.setMaxAge(3600);
cc.setPrivate(false);
} else if (path.startsWith("/api/user")) {
// ユーザー固有のデータは短めにキャッシュ
cc.setMaxAge(300);
cc.setPrivate(true);
} else {
// デフォルトはキャッシュなし
cc.setNoCache(true);
}
responseContext.getHeaders().add("Cache-Control", cc);
}
}
- 条件付き更新の実装
@Path("/resources")
public class ConditionalUpdateResource {
@PUT
@Path("/{id}")
public Response updateResource(
@PathParam("id") Long id,
@HeaderParam("If-Match") String ifMatch,
ResourceDTO resourceDTO
) {
Resource currentResource = resourceService.findById(id);
if (currentResource == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
// ETAGの検証
EntityTag currentEtag = new EntityTag(
Integer.toString(currentResource.hashCode())
);
if (ifMatch != null && !ifMatch.equals(currentEtag.getValue())) {
return Response.status(Response.Status.PRECONDITION_FAILED)
.entity("Resource has been modified")
.build();
}
// リソースの更新
Resource updatedResource = resourceService.update(id, resourceDTO);
// 新しいETAGの生成
EntityTag newEtag = new EntityTag(
Integer.toString(updatedResource.hashCode())
);
return Response.ok(updatedResource)
.tag(newEtag)
.build();
}
}
これらの実装例は、実際のプロジェクトでよく必要とされる機能を効率的に実装する方法を示しています。各実装には、セキュリティ、パフォーマンス、エラーハンドリングなどの重要な考慮事項が含まれており、実務で即座に活用できる形となっています。
JAX-RSによるマイクロサービス開発
マイクロサービスアーキテクチャにおける活用方法
- マイクロサービスの基本構造
@Path("/orders")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class OrderService {
@Inject
private OrderRepository orderRepository;
@Inject
@RestClient // MicroProfile Rest Client
private PaymentService paymentService;
@Inject
@RestClient
private InventoryService inventoryService;
@POST
public Response createOrder(OrderRequest request) {
// 在庫確認
InventoryStatus status = inventoryService.checkInventory(
request.getProductId(),
request.getQuantity()
);
if (!status.isAvailable()) {
return Response.status(Response.Status.CONFLICT)
.entity(new ErrorResponse("商品在庫不足"))
.build();
}
// 支払い処理
PaymentResult payment = paymentService.processPayment(
request.getPaymentDetails()
);
if (!payment.isSuccessful()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("支払い処理失敗"))
.build();
}
// 注文作成
Order order = orderRepository.create(
new Order(request, payment.getTransactionId())
);
return Response.status(Response.Status.CREATED)
.entity(order)
.build();
}
}
- サービスディスカバリの統合
@ApplicationScoped
public class ServiceDiscoveryConfig {
@Inject
private ConsulClient consulClient;
public void registerService() {
Registration registration = Registration.builder()
.id("order-service-" + UUID.randomUUID())
.name("order-service")
.address("localhost")
.port(8080)
.check(Registration.RegCheck.http("/health", 10))
.build();
consulClient.agentServiceRegister(registration);
}
}
サービス間通信の実践テクニック
- MicroProfile Rest Clientの活用
@Path("/payment")
@RegisterRestClient(configKey="payment-api")
public interface PaymentService {
@POST
@Path("/process")
PaymentResult processPayment(PaymentDetails details);
@GET
@Path("/status/{id}")
PaymentStatus getPaymentStatus(@PathParam("id") String transactionId);
}
@Path("/inventory")
@RegisterRestClient(configKey="inventory-api")
public interface InventoryService {
@GET
@Path("/check/{productId}")
InventoryStatus checkInventory(
@PathParam("productId") String productId,
@QueryParam("quantity") int quantity
);
}
- サーキットブレーカーの実装
@Path("/resilient")
public class ResilientService {
@Inject
@RestClient
private ExternalService externalService;
@GET
@Path("/data")
@Fallback(fallbackMethod = "getFallbackData")
@Timeout(500) // 500ミリ秒でタイムアウト
@Retry(maxRetries = 3)
@CircuitBreaker(
requestVolumeThreshold = 4,
failureRatio = 0.5,
delay = 1000,
successThreshold = 2
)
public Response getData() {
return Response.ok(externalService.fetchData()).build();
}
public Response getFallbackData() {
// フォールバックロジック
return Response.ok(new FallbackData()).build();
}
}
- 分散トレーシングの実装
@Provider
public class TracingFilter implements ContainerRequestFilter, ContainerResponseFilter {
@Inject
private Tracer tracer;
@Override
public void filter(ContainerRequestContext requestContext) {
Span span = tracer.buildSpan("http-request")
.withTag("http.method", requestContext.getMethod())
.withTag("http.url", requestContext.getUriInfo().getPath())
.start();
requestContext.setProperty("span", span);
}
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
Span span = (Span) requestContext.getProperty("span");
if (span != null) {
span.setTag("http.status_code",
responseContext.getStatus());
span.finish();
}
}
}
コンテナ化とデプロイメントの考慮点
- Dockerfileの最適化
# ビルドステージ
FROM maven:3.8.3-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package
# 実行ステージ
FROM openjdk:17-slim
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
# ヘルスチェックの設定
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
- Kubernetes設定
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: order-service
image: order-service:latest
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 15
env:
- name: PAYMENT_SERVICE_URL
valueFrom:
configMapKeyRef:
name: service-config
key: payment.service.url
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
- 設定管理
@ApplicationScoped
public class ConfigurationProvider {
@Inject
@ConfigProperty(name = "payment.service.url")
String paymentServiceUrl;
@Inject
@ConfigProperty(name = "inventory.service.url")
String inventoryServiceUrl;
@Inject
@ConfigProperty(name = "circuit.breaker.timeout",
defaultValue = "1000")
int circuitBreakerTimeout;
@Produces
@ApplicationScoped
public ServiceConfig createServiceConfig() {
return ServiceConfig.builder()
.paymentServiceUrl(paymentServiceUrl)
.inventoryServiceUrl(inventoryServiceUrl)
.circuitBreakerTimeout(circuitBreakerTimeout)
.build();
}
}
これらの実装例は、JAX-RSをマイクロサービスアーキテクチャで活用する際の主要なポイントを示しています。サービス間通信、レジリエンス、スケーラビリティ、運用性などの重要な側面をカバーしており、実際のプロジェクトでの参考として活用できます。特に、MicroProfileの機能を活用することで、より堅牢なマイクロサービスの構築が可能となります。