Chuyển đến nội dung chính

Bài 18: Hands-on — Xây dựng FHIR Server với HAPI FHIR

HAPI FHIR JPA Server setup với Spring Boot, PostgreSQL backend, cấu hình indexing và search parameters, resource validation, interceptors pattern, custom operations ($everything, $validate), bulk data export, Docker deployment, performance tuning.

🏗️ Kiến trúc — Bài 18 Bài 18: Hands-on — Xây dựng FHIR Server với HAPI FHIR

HL7 FHIR - Chuẩn Dữ liệu Y tế từ Cơ bản đến Nâng cao

Phần 6: Thực hành - Xây dựng hệ thống FHIR

xdev.asia

Xem bản video

1. Giới thiệu HAPI FHIR

HAPI FHIR là thư viện Java mã nguồn mở phổ biến nhất để xây dựng FHIR Server và Client. HAPI FHIR JPA Server cung cấp một FHIR Server hoàn chỉnh với persistence layer sử dụng JPA/Hibernate.

Thành phầnMô tả
HAPI FHIR CoreParser, model classes, client/server framework
JPA ServerFull FHIR server với database persistence
ValidationProfile-based validation engine
CLICommand-line tool cho migration, upload
FHIR VersionHỗ trợ DSTU2, STU3, R4, R4B, R5

2. Khởi tạo Project với Spring Boot

Maven Dependencies

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>

<properties>
    <hapi.fhir.version>7.4.0</hapi.fhir.version>
</properties>

<dependencies>
    <dependency>
        <groupId>ca.uhn.hapi.fhir</groupId>
        <artifactId>hapi-fhir-jpaserver-starter</artifactId>
        <version>${hapi.fhir.version}</version>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>
    <dependency>
        <groupId>ca.uhn.hapi.fhir</groupId>
        <artifactId>hapi-fhir-structures-r5</artifactId>
        <version>${hapi.fhir.version}</version>
    </dependency>
</dependencies>

Application Properties

# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/hapi_fhir
    username: hapi
    password: ${DB_PASSWORD}
    driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: false
        jdbc:
          batch_size: 20
        order_inserts: true
        order_updates: true

hapi:
  fhir:
    fhir_version: R5
    server_address: http://localhost:8080/fhir
    allow_multiple_delete: true
    allow_external_references: true
    default_page_size: 20
    max_page_size: 200
    validation:
      requests_enabled: true
      responses_enabled: false

3. Cấu hình FHIR Server

@Configuration
public class FhirServerConfig {

    @Bean
    public RestfulServer restfulServer(
            ApplicationContext context,
            FhirContext fhirContext) {
        
        RestfulServer server = new RestfulServer(fhirContext);
        server.setDefaultResponseEncoding(EncodingEnum.JSON);
        server.setDefaultPrettyPrint(true);
        
        // Register resource providers
        server.registerProviders(
            context.getBean(PatientResourceProvider.class),
            context.getBean(ObservationResourceProvider.class),
            context.getBean(EncounterResourceProvider.class)
        );
        
        // Register interceptors
        server.registerInterceptor(new ResponseHighlighterInterceptor());
        server.registerInterceptor(new CorsInterceptor());
        server.registerInterceptor(context.getBean(AuditInterceptor.class));
        
        return server;
    }
    
    @Bean
    public FhirContext fhirContext() {
        return FhirContext.forR5();
    }
}

4. Resource Providers

@Component
public class PatientResourceProvider implements IResourceProvider {
    
    @Autowired
    private PatientRepository patientRepo;
    
    @Override
    public Class<Patient> getResourceType() {
        return Patient.class;
    }
    
    @Read
    public Patient read(@IdParam IdType theId) {
        return patientRepo.findById(theId.getIdPart())
            .orElseThrow(() -> new ResourceNotFoundException(theId));
    }
    
    @Create
    public MethodOutcome create(@ResourceParam Patient patient) {
        // Validate trước khi lưu
        ValidationResult result = validator.validateWithResult(patient);
        if (!result.isSuccessful()) {
            throw new UnprocessableEntityException(
                result.toOperationOutcome());
        }
        
        Patient saved = patientRepo.save(patient);
        return new MethodOutcome()
            .setId(saved.getIdElement())
            .setCreated(true);
    }
    
    @Search
    public List<Patient> searchByName(
            @RequiredParam(name = Patient.SP_NAME) StringParam name) {
        return patientRepo.findByNameContaining(name.getValue());
    }
    
    @Search
    public List<Patient> searchByIdentifier(
            @RequiredParam(name = Patient.SP_IDENTIFIER) 
            TokenParam identifier) {
        return patientRepo.findByIdentifier(
            identifier.getSystem(), identifier.getValue());
    }
    
    @Operation(name = "$everything", idempotent = true)
    public Bundle patientEverything(@IdParam IdType patientId) {
        Bundle bundle = new Bundle();
        bundle.setType(Bundle.BundleType.SEARCHSET);
        
        Patient patient = read(patientId);
        bundle.addEntry().setResource(patient);
        
        // Thêm Encounters, Observations, Conditions...
        encounterRepo.findByPatient(patientId.getIdPart())
            .forEach(e -> bundle.addEntry().setResource(e));
        observationRepo.findBySubject(patientId.getIdPart())
            .forEach(o -> bundle.addEntry().setResource(o));
        
        return bundle;
    }
}

5. Interceptors

HAPI FHIR sử dụng Interceptor pattern để hook vào lifecycle của request.

@Component
public class AuditInterceptor {
    
    @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
    public void logIncomingRequest(
            RequestDetails requestDetails) {
        log.info("FHIR Request: {} {} from {}",
            requestDetails.getRequestType(),
            requestDetails.getCompleteUrl(),
            requestDetails.getAttribute("remoteAddr"));
    }
    
    @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
    public void auditCreate(IBaseResource resource,
            RequestDetails requestDetails) {
        AuditEvent audit = new AuditEvent();
        audit.setAction(AuditEvent.AuditEventAction.C);
        audit.setRecorded(new Date());
        
        AuditEvent.AuditEventAgentComponent agent = 
            audit.addAgent();
        agent.setRequestor(true);
        // Set agent from auth context
        
        AuditEvent.AuditEventEntityComponent entity = 
            audit.addEntity();
        entity.setWhat(new Reference(
            resource.getIdElement().toUnqualifiedVersionless()));
        
        auditRepo.save(audit);
    }
}

Validation Interceptor

@Component
public class ValidationInterceptor {
    
    @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
    @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
    public void validateResource(IBaseResource resource) {
        FhirValidator validator = fhirContext.newValidator();
        
        // Thêm profile validation
        IValidatorModule module = new FhirInstanceValidator(
            validationSupport);
        validator.registerValidatorModule(module);
        
        ValidationResult result = validator.validateWithResult(resource);
        
        if (!result.isSuccessful()) {
            OperationOutcome oo = (OperationOutcome) 
                result.toOperationOutcome();
            throw new UnprocessableEntityException(
                fhirContext, oo);
        }
    }
}

6. Custom Search Parameters

{
  "resourceType": "SearchParameter",
  "url": "http://xdev.asia/fhir/SearchParameter/patient-cccd",
  "name": "cccd",
  "status": "active",
  "description": "Tìm kiếm bệnh nhân theo số CCCD",
  "code": "cccd",
  "base": ["Patient"],
  "type": "token",
  "expression": "Patient.identifier.where(system='http://xdev.asia/fhir/sid/cccd').value"
}

Đăng ký search parameter:

curl -X POST http://localhost:8080/fhir/SearchParameter \
  -H "Content-Type: application/fhir+json" \
  -d @search-parameter-cccd.json

# Reindex để áp dụng
curl -X POST http://localhost:8080/fhir/$reindex \
  -H "Content-Type: application/fhir+json" \
  -d '{"resourceType":"Parameters","parameter":[{"name":"url","valueString":"Patient"}]}'

7. Bulk Data Export

# Khởi tạo export Patient và Observation
curl -X GET 'http://localhost:8080/fhir/$export' \
  -H "Accept: application/fhir+json" \
  -H "Prefer: respond-async" \
  -H "Content-Type: application/fhir+json" \
  --data '{
    "resourceType": "Parameters",
    "parameter": [
      {"name": "_type", "valueString": "Patient,Observation"},
      {"name": "_outputFormat", "valueString": "application/fhir+ndjson"}
    ]
  }'

# Response Header: Content-Location: http://localhost:8080/fhir/$export-poll/abc123

# Kiểm tra trạng thái
curl http://localhost:8080/fhir/\$export-poll/abc123

# Download kết quả khi hoàn tất
curl -o patients.ndjson http://localhost:8080/fhir/bulk/abc123/Patient

8. Docker Deployment

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/hapi-fhir-server.jar app.jar

ENV JAVA_OPTS="-Xmx2g -Xms1g"
ENV SPRING_PROFILES_ACTIVE=production

EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
# docker-compose.yml
services:
  fhir-server:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/hapi_fhir
      - SPRING_DATASOURCE_USERNAME=hapi
      - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
      - HAPI_FHIR_SERVER_ADDRESS=https://fhir.xdev.asia/fhir
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/fhir/metadata"]
      interval: 30s
      timeout: 10s
      retries: 5
  
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: hapi_fhir
      POSTGRES_USER: hapi
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U hapi"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

9. Performance Tuning

Cấu hìnhGiá trị khuyến nghịMô tả
hibernate.jdbc.batch_size20-50Batch insert/update
max_page_size200Giới hạn kết quả trả về
reuse_cached_search_results60000 (ms)Cache search results
JVM Heap2-4 GBTùy theo số lượng resources
Connection pool20-50HikariCP connections
PostgreSQL shared_buffers25% RAMDatabase buffer

10. Tổng kết

  • HAPI FHIR JPA Server — Full-featured FHIR server built on Spring Boot + PostgreSQL

  • Resource Providers — Implement CRUD, search, custom operations

  • Interceptors — Hook vào request lifecycle cho audit, validation, security

  • Custom Search Parameters — Mở rộng tìm kiếm theo nhu cầu (CCCD, BHYT)

  • Bulk Data Export — Export lượng lớn data ở dạng NDJSON

  • Docker — Production deployment với docker-compose