728x90
petclinic을 예제 REST API로 만든 git이 있어서 살펴봤다.
위 git 사이트에서 project를 받아서 intellij로 실행했다.
java: package org.springframework.samples.petclinic.rest.dto does not exist
위와 같은 에러가 발생했다.
코드를 다시 받아 아래와 같은 방법으로 해결했다.
해결방법
- 터미널에 nvm install 치면 target > generated-sources > openapi > src > … > api, dto 폴더 생김
- target 폴더 하위 폴더 중 java 폴더를 우 클릭후 Mark directory as > sources root 로 설정
- File > restart IDE 로 재시작 해줌
- 다시 PetClinicApplication 우클릭 Run 실행
- http://localhost:9966/petclinic 접속해서 API 스웨거 페이지가 잘 나오는지 확인
눈에 띄는 특징 정리
특징
- Entity → Dto 맵핑을 위해 mapstruct 라이브러리를 사용했다.
- RepositoryImpl 마다 @Profile 을 통해 jdbc, jpa, springdatajpa 3개의 구현체의 예시를 만들어 놓았다.
Entity 에서 눈에 띄는 점
entity 내부에서 field 접근 시 처리해야할 로직이 있으면 protected 메서드를 사용
protected Set<Pet> getPetsInternal() {
if (this.pets == null) {
this.pets = new HashSet<>();
}
return this.pets;
}
외부로 노출 시 수정 불가한 unmodifiableList
public List<Pet> getPets() {
List<Pet> sortedPets = new ArrayList<>(getPetsInternal());
PropertyComparator.sort(sortedPets, new MutableSortDefinition("name", true, true));
return Collections.unmodifiableList(sortedPets);
}
Entity 내부 collection에 새로운 object 추가 시 해당 object에 자기 자신도 등록
public void addPet(Pet pet) {
getPetsInternal().add(pet);
pet.setOwner(this);
}
spring-data-jpa에서 눈에 띄는 점
- (자동구현될) 필요한 메서드 Interface / 사용자가 구현할 override Interface /
Spring Data JPA 를 활용해 구현할 Interface 로 나눴음 - @Query를 사용
public interface PetRepository {
Collection<Pet> findAll() throws DataAccessException;
void delete(Pet pet) throws DataAccessException;
}
@Profile("spring-data-jpa")
public interface PetRepositoryOverride {
void delete(Pet pet);
}
@Profile("spring-data-jpa")
public class SpringDataPetRepositoryImpl implements PetRepositoryOverride {
@PersistenceContext
private EntityManager em;
@Override
public void delete(Pet pet) {
String petId = pet.getId().toString();
this.em.createQuery("DELETE FROM Visit visit WHERE pet.id=" + petId).executeUpdate();
this.em.createQuery("DELETE FROM Pet pet WHERE id=" + petId).executeUpdate();
if (em.contains(pet)) {
em.remove(pet);
}
}
}
@Profile("spring-data-jpa")
public interface SpringDataPetRepository extends PetRepository, Repository<Pet, Integer>, PetRepositoryOverride {
@Override
@Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name")
List<PetType> findPetTypes() throws DataAccessException;
}
- 2개 이상의 쿼리 작성 필요로 jpa entitymanager.createQuery 사용
- 연관 entity 관련 동작 로직을 repository 단에서 처리
- ex. pet을 삭제하면 pet이 방문한 장소도 함께 삭제
Controller에서 눈에 띄는 점
- ControllerAdvice 사용
- @PreAuthorize 으로 권한 확인
- readme에 있는 것처럼 API First 접근 방식을 사용한다.
- build를 하면 openapi.yml에 작성한대로 API template interface가 생성된다.
- 인터페이스를 Controller에서 상속받아 구현한다.
- 처음 보는 방식이었는데, Controller 메서드에 어노테이션 url이 나와있지 않아서 불편했다.
생성된 API template interface
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-04-29T02:13:49.482423300+09:00[Asia/Seoul]")
@Validated
@Tag(name = "owners", description = "Endpoints related to pet owners.")
public interface OwnersApi {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
/**
* POST /owners : Adds a pet owner
* Records the details of a new pet owner.
*
* @param ownerFieldsDto The pet owner (required)
* @return The pet owner was sucessfully added. (status code 201)
* or Bad request. (status code 400)
* or Server error. (status code 500)
*/
@Operation(
operationId = "addOwner",
summary = "Adds a pet owner",
description = "Records the details of a new pet owner.",
tags = { "owner" },
responses = {
@ApiResponse(responseCode = "201", description = "The pet owner was sucessfully added.", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = OwnerDto.class))
}),
@ApiResponse(responseCode = "400", description = "Bad request.", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = RestErrorDto.class))
}),
@ApiResponse(responseCode = "500", description = "Server error.", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = RestErrorDto.class))
})
}
)
@RequestMapping(
method = RequestMethod.POST,
value = "/owners",
produces = { "application/json" },
consumes = { "application/json" }
)
default ResponseEntity<OwnerDto> addOwner(
@Parameter(name = "OwnerFieldsDto", description = "The pet owner", required = true) @Valid @RequestBody OwnerFieldsDto ownerFieldsDto
) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "null";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
컨트롤러
@RestController
@CrossOrigin(exposedHeaders = "errors, content-type")
@RequestMapping("/api")
public class OwnerRestController implements OwnersApi {
private final ClinicService clinicService;
private final OwnerMapper ownerMapper;
private final PetMapper petMapper;
private final VisitMapper visitMapper;
public OwnerRestController(ClinicService clinicService,
OwnerMapper ownerMapper,
PetMapper petMapper,
VisitMapper visitMapper) {
this.clinicService = clinicService;
this.ownerMapper = ownerMapper;
this.petMapper = petMapper;
this.visitMapper = visitMapper;
}
@PreAuthorize("hasRole(@roles.OWNER_ADMIN)")
@Override
public ResponseEntity<OwnerDto> addOwner(OwnerFieldsDto ownerFieldsDto) {
HttpHeaders headers = new HttpHeaders();
Owner owner = ownerMapper.toOwner(ownerFieldsDto);
this.clinicService.saveOwner(owner);
OwnerDto ownerDto = ownerMapper.toOwnerDto(owner);
headers.setLocation(UriComponentsBuilder.newInstance()
.path("/api/owners/{id}").buildAndExpand(owner.getId()).toUri());
return new ResponseEntity<>(ownerDto, headers, HttpStatus.CREATED);
}
}
그 외
간단한 security 적용
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // Enable @PreAuthorize method-level security
@ConditionalOnProperty(name = "petclinic.security.enable", havingValue = "true")
public class BasicAuthenticationConfig {
@Autowired
private DataSource dataSource;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authz) -> authz
.anyRequest().authenticated()
)
.httpBasic()
.and()
.csrf()
.disable();
// @formatter:on
return http.build();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// @formatter:off
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username,password,enabled from users where username=?")
.authoritiesByUsernameQuery("select username,role from roles where username=?");
// @formatter:on
}
}
간단한 AOP 적용
- repository 메서드 호출 횟수 및 동작 시간 기록
- @Around는 메서드 호출을 감싼다.
- joinPoint.proceed() 는 메서드의 실행을 의미
@ManagedResource("petclinic:type=CallMonitor")
@Aspect
public class CallMonitoringAspect {
private boolean enabled = true;
private int callCount = 0;
private long accumulatedCallTime = 0;
@ManagedAttribute
public boolean isEnabled() {
return enabled;
}
@ManagedAttribute
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@ManagedOperation
public void reset() {
this.callCount = 0;
this.accumulatedCallTime = 0;
}
@ManagedAttribute
public int getCallCount() {
return callCount;
}
@ManagedAttribute
public long getCallTime() {
if (this.callCount > 0)
return this.accumulatedCallTime / this.callCount;
else
return 0;
}
@Around("within(@org.springframework.stereotype.Repository *)")
public Object invoke(ProceedingJoinPoint joinPoint) throws Throwable {
if (this.enabled) {
StopWatch sw = new StopWatch(joinPoint.toShortString());
sw.start("invoke");
try {
return joinPoint.proceed();
} finally {
sw.stop();
synchronized (this) {
this.callCount++;
this.accumulatedCallTime += sw.getTotalTimeMillis();
}
}
} else {
return joinPoint.proceed();
}
}
}
728x90
'Back-End > Spring' 카테고리의 다른 글
[Spring] pet clinic - 스프링 대표 예제 프로젝트 (0) | 2023.04.08 |
---|---|
[Spring Boot] SpringApplication (0) | 2023.03.31 |
[Spring Boot] Spring Boot - Web(Servelet Web Applications) (0) | 2023.03.30 |
[Spring Boot] 스프링 부트 사용하기 (0) | 2023.03.30 |
MVC 컨트롤러 List로 받기 (0) | 2021.03.14 |
댓글