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ần | Mô tả |
|---|---|
| HAPI FHIR Core | Parser, model classes, client/server framework |
| JPA Server | Full FHIR server với database persistence |
| Validation | Profile-based validation engine |
| CLI | Command-line tool cho migration, upload |
| FHIR Version | Hỗ 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ình | Giá trị khuyến nghị | Mô tả |
|---|---|---|
| hibernate.jdbc.batch_size | 20-50 | Batch insert/update |
| max_page_size | 200 | Giới hạn kết quả trả về |
| reuse_cached_search_results | 60000 (ms) | Cache search results |
| JVM Heap | 2-4 GB | Tùy theo số lượng resources |
| Connection pool | 20-50 | HikariCP connections |
| PostgreSQL shared_buffers | 25% RAM | Database 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