티스토리 뷰
SpringBoot I - 생성/설정, RESTful API, JPA, Transaction, Cache, Async Process,
마이스토리 2016. 5. 26. 17:44>> SpringBoot 프로젝트 생성 및 설정
> Project Wizard로 Spring Starter Project 생성
( maven 및 spring eclipse plugin 최신버전으로 update)
> pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>lyj</groupId>
<artifactId>sample-springBoot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>sample-springBoot</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
> Spring Boot Application 클래스 작성
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleSpringBootApplication {
public static void main(String[] args) throws Exception{
SpringApplication.run(SampleSpringBootApplication.class, args);
}
}
> spring boot Application 실행방법
1. run as > java application : application클래스 메인메소드
2. run as > Spring boot App : spring eclipse plugin 설치된 경우
3. run as > maven build... : goal은 spring-boot:run 입력, 또는 $> mvn spring-boot:run 명령어 실행
> 정상실행 되고, http://localhost:8080 페이지 접속하면 아래와 같이 표시됨.
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
No message available
> Jar파일 packaging 및 실행
1. mvn clean package 실행 --> /target 디렉토리 jar파일 생성 (ex: sample-springBoot-0.0.1-SNAPSHOT.jar)
2. java -jar sample-springBoot-0.0.1-SNAPSHOT.jar 명령어 실행
* Maven 설치
1. maven 다운로드 및 압축풀기 : (https://maven.apache.org/download.cgi)
2. PATH 환경변수에 %MAVEN_HOME%/bin 디렉토리 추가
3. JAVA_HOME 환경변수 설정필수
4. $> mvn -v 명령어로 확인
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T01:41:47+09:00)
Maven home: C:\Dev\apache-maven-3.3.9\bin\..
Java version: 1.7.0_80, vendor: Oracle Corporation
Java home: C:\Program Files\Java\jdk1.7.0_80\jre
Default locale: ko_KR, platform encoding: MS949
OS name: "windows 7", version: "6.1", arch: "x86", family: "windows"
>> RESTful 서비스 개발
> Controller 및 Service 예제
* Controller 소스
package lyj.sample.web;
import java.math.BigInteger;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import lyj.sample.model.Greeting;
import lyj.sample.service.GreetingService;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
@Resource
private GreetingService greetingService;
@RequestMapping(value="/api/greetings", method=RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Collection<Greeting>> getGreetings(){
Collection<Greeting> greetings = greetingService.findAll();
return new ResponseEntity<Collection<Greeting>>(greetings, HttpStatus.OK);
}
@RequestMapping(value="/api/greetings/{id}", method=RequestMethod.GET, produces=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Greeting> getGreeting(@PathVariable("id") BigInteger id){
Greeting greeting = greetingService.findOne(id);
if (greeting == null){
return new ResponseEntity<Greeting>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Greeting>(greeting, HttpStatus.OK);
}
@RequestMapping(value="/api/greetings", method=RequestMethod.POST, consumes=MediaType.APPLICATION_JSON_VALUE, produces=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Greeting> createGreeting(@RequestBody Greeting greeting){
Greeting savedGreeting = greetingService.create(greeting);
return new ResponseEntity<Greeting>(savedGreeting, HttpStatus.CREATED);
}
@RequestMapping(value="/api/greetings/{id}", method=RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Greeting> upateGreeting(@RequestBody Greeting greeting){
Greeting updatedGreeting = greetingService.update(greeting);
if (updatedGreeting == null){
return new ResponseEntity<Greeting>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<Greeting>(updatedGreeting, HttpStatus.OK);
}
@RequestMapping(value="/api/greetings/{id}", method=RequestMethod.DELETE, consumes=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Greeting> deleteGreeting(@PathVariable("id") BigInteger id, @RequestBody Greeting greeting){
greetingService.delete(id);
return new ResponseEntity<Greeting>(HttpStatus.NO_CONTENT);
}
}
> Service 소스 예제
package lyj.sample.service;
import java.math.BigInteger;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import lyj.sample.model.Greeting;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
private static BigInteger nextId;
private static Map<BigInteger, Greeting> greetingMap;
private static Greeting save(Greeting greeting){
if (greetingMap == null){
greetingMap = new HashMap<BigInteger, Greeting>();
nextId = BigInteger.ONE;
}
// If Update.....
if (greeting.getId() != null){
Greeting oldGreeting = greetingMap.get(greeting.getId());
if (oldGreeting == null){
return null;
}
greetingMap.remove(greeting.getId());
greetingMap.put(greeting.getId(), greeting);
return greeting;
}
// If Create.....
greeting.setId(nextId);
nextId = nextId.add(BigInteger.ONE);
greetingMap.put(greeting.getId(), greeting);
return greeting;
}
private static boolean remove(BigInteger id){
Greeting deletedGreeging = greetingMap.remove(id);
if (deletedGreeging == null){
return false;
}
return true;
}
static {
Greeting g1 = new Greeting();
g1.setText("Hello World!");
save(g1);
Greeting g2 = new Greeting();
g2.setText("Hola Mundo!");
save(g2);
}
public Collection<Greeting> findAll(){
Collection<Greeting> greetings = greetingMap.values();
return greetings;
}
public Greeting findOne(BigInteger id){
Greeting greeting = greetingMap.get(id);
return greeting;
}
public Greeting create(Greeting greeting){
Greeting savedGreeting = save(greeting);
return savedGreeting;
}
public Greeting update(Greeting greeting){
Greeting updatedGreeting = save(greeting);
return updatedGreeting;
}
public void delete(BigInteger id){
remove(id);
}
}
>> Persistance Layer with JPA
> depenency 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
> Repository 인터페이스 작성
package lyj.sample.repository;
import java.math.BigInteger;
import lyj.sample.model.Greeting;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface GreetingRepository extends JpaRepository<Greeting, BigInteger> {
}
> 서비스 메소드 repository 사용
@Service
public class GreetingService {
@Autowired
GreetingRepository greetingRepository;
public Collection<Greeting> findAll(){
Collection<Greeting> greetings = greetingRepository.findAll();
return greetings;
}
public Greeting findOne(BigInteger id){
Greeting greeting = greetingRepository.findOne(id);
return greeting;
}
public Greeting create(Greeting greeting){
if (greeting.getId() != null){
// Cannot create Greeting with specified ID value
return null;
}
Greeting savedGreeting = greetingRepository.save(greeting);
return savedGreeting;
}
public Greeting update(Greeting greeting){
Greeting greetingPersisted = findOne(greeting.getId());
if (greetingPersisted == null){
// Cannot update Greeting that hasn't been persisted.
return null;
}
Greeting updatedGreeting = greetingRepository.save(greeting);
return updatedGreeting;
}
public void delete(BigInteger id){
greetingRepository.delete(id);
}
}
> 도메인 클래스 @Entity 어노테이션
@Entity
public class Greeting {
@Id
@GeneratedValue
private BigInteger id;
private String text;
> hsqldb script 파일 작성
* src\main\resources\data\hsqldb\schema.sql
DROP TABLE Greeting IF EXISTS;
CREATE TABLE Greeting(
id NUMERIC(19) GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1) NOT NULL,
text VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
)
* src\main\resources\data\hsqldb\data.sql
INSERT INTO Greeting(text) VALUES ('Hello World!');
INSERT INTO Greeting(text) VALUES ('Hola Mundo!');
> application.propertis 에 data source config 추가
# Hibernate
spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.DefaultNamingStrategy
spring.jpa.hibernate.ddl-auto=validate
# Initialization
spring.datasource.schema=classpath:/data/hsqldb/schema.sql
spring.datasource.data=classpath:/data/hsqldb/data.sql
>> Transaction Management
> spring boot main class에 @EnableTransactionManagement 어노테이션 추가
@SpringBootApplication
@EnableTransactionManagement
public class SampleSpringBootApplication {
public static void main(String[] args) throws Exception{
SpringApplication.run(SampleSpringBootApplication.class, args);
}
}
> Service 클래스 및 메소드에 @Transactional 어노테이션 추가. 의도적 rollback 상황 exception 로직 추가
@Service
@Transactional(propagation=Propagation.SUPPORTS, readOnly=true)
public class GreetingService {
.....
@Transactional(propagation=Propagation.REQUIRED, readOnly=false)
public Greeting create(Greeting greeting){
if (greeting.getId() != null){
// Cannot create Greeting with specified ID value
return null;
}
Greeting savedGreeting = greetingRepository.save(greeting);
// Illustrate Tx rollback
if (savedGreeting.getId() == BigInteger.valueOf(4L)){
throw new RuntimeException("Roll me back!!");
}
return savedGreeting;
}
.....
>> Cache Management
> Main Configuration Class에 @EnableCaching 어노테이션 및 CacheManager Bean생성
@SpringBootApplication
@EnableTransactionManagement
@EnableCaching
public class SampleSpringBootApplication {
public static void main(String[] args) throws Exception{
SpringApplication.run(SampleSpringBootApplication.class, args);
}
@Bean
public CacheManager cacheManager(){
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager("greetings");
return cacheManager;
}
}
> Service method에 @Cacheable, @CachePut, @CacheEvic 어노테이션 적용
@Cacheable(value="greetings", key="#id")
public Greeting findOne(BigInteger id){
@CachePut(value="greetings", key="#result.id")
public Greeting create(Greeting greeting){
@CachePut(value="greetings", key="#greeting.id")
public Greeting update(Greeting greeting){
@CacheEvict(value="greetings", key="#id")
public void delete(BigInteger id){
> Google guava cache 사용하기
> pom.xml dependency 추가
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- http://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
> main configuration class에 cachemanager bean 변경
@Bean
public CacheManager cacheManager(){
GuavaCacheManager cacheManager = new GuavaCacheManager("greetings");
return cacheManager;
}
>> Scheduled Process
> application main configure class 에 @@EnableScheduling 어노테이션 추가
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableTransactionManagement
@EnableCaching
@EnableScheduling
public class SampleSpringBootApplication {
> schedule bean class 작성
package lyj.sample.batch;
import java.util.Collection;
import lyj.sample.model.Greeting;
import lyj.sample.service.GreetingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class GreetingBatchBean {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private GreetingService greetingService;
@Scheduled(cron="0,30 * * * * *")
public void cronJob(){
logger.info("> cronJob");
// Add scheduled logic here
Collection<Greeting> greetings = greetingService.findAll();
logger.info("There are {} greetings in the data store.", greetings.size());
logger.info("< cronJob");
}
@Scheduled(initialDelay = 5000
, fixedRate = 15000 //이전 수행 시작 기준. 이전 배치가 완료되어야 다음 배치 실행됨.
//, fixedDelay = 15000 //이전 수행 종료 기준.
)
public void fixedRateJobWithInitialDelay(){
logger.info("> fixedRateJobWithInitialDelay");
long pause = 5000;
long start = System.currentTimeMillis();
do{
if (start + pause < System.currentTimeMillis()){
break;
}
}while(true);
logger.info("Processing time was {} seconds.", pause / 1000);
logger.info("< fixedRateJobWithInitialDelay");
}
}
>> Asynchronous Process
> application main confugure class 에 @EnableAsync 어노테이션 추가
> AsyncResponse class 작성
package lyj.sample.util;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class AsyncResponse<V> implements Future<V> {
private V value;
private Exception executionException;
private boolean isCompletedExceptionally = false;
private boolean isCancelled = false;
private boolean isDone = false;
private long checkCompletedInterval = 100;
public AsyncResponse() {
}
public AsyncResponse(V val){
this.value = val;
this.isDone = true;
}
public AsyncResponse(Throwable ex){
this.executionException = new ExecutionException(ex);
this.isCompletedExceptionally = true;
this.isDone = true;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
this.isCancelled = true;
this.isDone = true;
return false;
}
@Override
public boolean isCancelled() {
return this.isCancelled;
}
public boolean isCompletedExceptionally(){
return this.isCompletedExceptionally;
}
@Override
public boolean isDone() {
return this.isDone;
}
@Override
public V get() throws InterruptedException, ExecutionException {
block(0);
if (isCancelled()){
throw new CancellationException();
}
if (isCompletedExceptionally()){
throw new ExecutionException(this.executionException);
}
if (isDone()){
return this.value;
}
throw new InterruptedException();
}
@Override
public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
long timeoutInMillis = unit.toMillis(timeout);
block(timeoutInMillis);
if (isCancelled()){
throw new CancellationException();
}
if (isCompletedExceptionally()){
throw new ExecutionException(this.executionException);
}
if (isDone()){
return this.value;
}
throw new InterruptedException();
}
public boolean complete(V val){
this.value = val;
this.isDone = true;
return true;
}
public boolean completeExceptionally(Throwable ex){
this.value = null;
this.executionException = new ExecutionException(ex);
this.isCompletedExceptionally = true;
this.isDone = true;
return true;
}
public void setCheckCompletedInterval(long millis){
this.checkCompletedInterval = millis;
}
private void block(long timeout) throws InterruptedException{
long start = System.currentTimeMillis();
// Block until done, cancelled, or the timeout is exceeded
while(!isDone() && !isCancelled()){
if (timeout > 0){
long now = System.currentTimeMillis();
if (now > start + timeout){
break;
}
}
Thread.sleep(checkCompletedInterval);
}
}
}
> Async Service 작성
package lyj.sample.service;
import java.util.concurrent.Future;
import lyj.sample.model.Greeting;
import lyj.sample.util.AsyncResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class EmailService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
public Boolean send(Greeting greeting){
logger.info("> send");
Boolean success = Boolean.FALSE;
// Simulate method execution time
long pause = 5000;
try{
Thread.sleep(pause);
} catch(Exception e){
// do nothing
}
logger.info("Processing time was {} seconds.", pause/1000);
success = Boolean.TRUE;
logger.info("< send");
return success;
}
@Async
public void sendAsync(Greeting greeting){
logger.info("> sendAsync");
try{
send(greeting);
} catch(Exception e){
logger.warn("Excecution caught sending asynchronous mail.", e);
}
logger.info("< sendAsync");
}
@Async
public Future<Boolean> sendAsyncWithResult(Greeting greeting){
logger.info("> sendAsyncWithResult");
AsyncResponse<Boolean> response = new AsyncResponse<Boolean>();
try{
Boolean success = send(greeting);
response.complete(success);
} catch(Exception e){
logger.warn("Excecution caught sending asynchronous mail.", e);
response.completeExceptionally(e);
}
logger.info("< sendAsyncWithResult");
return response;
}
}
> Controller에서 호출
@RequestMapping(value="/api/greeting/{id}/send", method=RequestMethod.POST, produces=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Greeting> sendGreeting(@PathVariable("id") BigInteger id,
@RequestParam(value="wait", defaultValue="false") boolean waitForAsyncResult){
logger.info("> sendGreeting : wait=" + waitForAsyncResult);
Greeting greeting = null;
try{
greeting = greetingService.findOne(id);
if (greeting == null){
return new ResponseEntity<Greeting>(HttpStatus.NOT_FOUND);
}
if (waitForAsyncResult){
Future<Boolean> asyncResponse = emailService.sendAsyncWithResult(greeting);
boolean emailSent = asyncResponse.get();
logger.info("- greeting email sent? {}", emailSent);
} else{
emailService.sendAsync(greeting);
}
} catch(Exception e){
logger.error("A problem occured sending the Greeting.", e);
return new ResponseEntity<Greeting>(HttpStatus.INTERNAL_SERVER_ERROR);
}
logger.info("<sendGreeting");
return new ResponseEntity<Greeting>(greeting, HttpStatus.OK);
}
테스트 결과
http://localhost:8080/api/greeting/2/send?wait=true 호출시
2016-05-31 18:13:12.381 INFO 4800 --- [nio-8080-exec-5] lyj.sample.web.GreetingController : > sendGreeting : wait=true
2016-05-31 18:13:12.383 INFO 4800 --- [cTaskExecutor-5] lyj.sample.service.EmailService : > sendAsyncWithResult
2016-05-31 18:13:12.383 INFO 4800 --- [cTaskExecutor-5] lyj.sample.service.EmailService : > send
2016-05-31 18:13:17.397 INFO 4800 --- [cTaskExecutor-5] lyj.sample.service.EmailService : Processing time was 5 seconds.
2016-05-31 18:13:17.398 INFO 4800 --- [cTaskExecutor-5] lyj.sample.service.EmailService : < send
2016-05-31 18:13:17.398 INFO 4800 --- [cTaskExecutor-5] lyj.sample.service.EmailService : < sendAsyncWithResult
2016-05-31 18:13:17.398 INFO 4800 --- [nio-8080-exec-5] lyj.sample.web.GreetingController : - greeting email sent? true
2016-05-31 18:13:17.398 INFO 4800 --- [nio-8080-exec-5] lyj.sample.web.GreetingController : < sendGreeting
http://localhost:8080/api/greeting/2/send?wait=false 호출시
2016-05-31 18:12:38.014 INFO 4800 --- [nio-8080-exec-4] lyj.sample.web.GreetingController : > sendGreeting : wait=false
2016-05-31 18:12:38.016 INFO 4800 --- [nio-8080-exec-4] lyj.sample.web.GreetingController : < sendGreeting
2016-05-31 18:12:38.020 INFO 4800 --- [cTaskExecutor-4] lyj.sample.service.EmailService : > sendAsync
2016-05-31 18:12:38.020 INFO 4800 --- [cTaskExecutor-4] lyj.sample.service.EmailService : > send
2016-05-31 18:12:43.020 INFO 4800 --- [cTaskExecutor-4] lyj.sample.service.EmailService : Processing time was 5 seconds.
2016-05-31 18:12:43.020 INFO 4800 --- [cTaskExecutor-4] lyj.sample.service.EmailService : < send
2016-05-31 18:12:43.020 INFO 4800 --- [cTaskExecutor-4] lyj.sample.service.EmailService : < sendAsync
- Total
- Today
- Yesterday