@ContextConfiguration은 통합 테스트에서 클래스 레벨 메타데이터(xml 파일 or javaConfig 파일)를 정의한다. 다시 말해, context를 로드하는데 사용되는 annotated class(@Configuration 클래스)나 application context resource locations(classpath에 위치한 XML 설정 파일)들을 선언한다.
또한 @ContextConfiguration은 ContextLoader 전략을 사용할 수 있다. 하지만 일반적으로 로더를 직접 명시할 필요는 없다. default loader가 initializers 뿐만 아니라 resource locations 또는 annotated classes를 지원하기 때문이다.
문제 발생
Spring Boot에서 @ContextConfiguration(classes = Application.class)만 설정했더니
[main] DEBUG org.springframework.core.type.classreading.AnnotationAttributesReadingVisitor -
Failed to class-load type while reading annotation metadata.
This is a non-fatal error, but certain annotation metadata may be unavailable.
java.lang.ClassNotFoundException:
org.springframework.data.web.config.EnableSpringDataWebSupport
...
java.lang.ClassNotFoundException:
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
...
위와 같은 에러뿐만 아니라 기본적인 Autowired 설정도 안되고, BeanCreationException도 발생했다.
원인은 애노테이션이 classpath에 없기 때문에 클래스가 로드될 때 JVM이 drop 시켜서 발생한 문제였다.
해결 방법1
Spring Boot에서는 기존의 @ContextConfiguration 대신 @SpringApplicationConfiguration을 제공한다. ApplicationContext 설정을 @SpringApplicationConfiguration으로 사용하면 SpringApplication 으로 생성되고 추가적인 Spring Boot feature들을 얻을 수 있다.
ConfigFileApplicationContextInitializer는 Spring Boot application.properties파일을 로드해 테스트 코드에 적용한다. @SpringApplicationConfiguration가 제공하는 full feature들이 필요 없을 때 사용된다.
cf) spring boot 1.4
직접적인 Configuration 설정 없이도 @*Test 애노테이션이 자동으로 primary configuration을 찾는다(테스트가 포함된 패키지로부터 @SpringBootApplication또는 @SpringBootConfiguration 애노테이션 클래스를 찾는다).
cf) 방법4에서 @SpringBootTest에서 classes 속성을 생략하면 inner-classes에서 @Configuration을 제일 먼저 로드하려 시도하고, 없으면 @SpringBootApplication class를 찾는다.
@WebApplicationContext
WebApplicationContext을 생성할 수 있게 해준다.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class AccountControllerTest {
@Autowired
WebApplicationContext wac;
MockMvc mockMvc;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
...
}
2. dataSource
메이븐 pom.xml에서 에서 spring-boot-starter-data-jpa 를 추가하면 그안에 spring-boot-starter-jdbc가 있고 그 안에 tomcat-jdbc가 있다.
Spring Boot에서는 DataSource 관리를 위한 구현체로써 tomcat-jdbc(The Tomcat JDBC Pool) 을 default로 제공한다.
근데 실서버에 배포 했을 때 에러가 반복해서 발생했고 그 주기도 일정치 않아서 재연이 쉽지 않았다. validationQuery: select 1을 설정했음에도 connection이 자꾸 닫히는 문제가 발생했다.
[2016-05-19 17:37:40.187] boot - 11886 ERROR [http-nio-8080-exec-3] --- SqlExceptionHelper: No operations allowed after connection closed.
[2016-05-19 17:37:40.188] boot - 11886 ERROR [http-nio-8080-exec-3] --- TransactionInterceptor: Application exception overridden by rollback exception
javax.persistence.PersistenceException: org.hibernate.exception.JDBCConnectionException: could not prepare statement
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1763)
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1677)
at org.hibernate.jpa.internal.QueryImpl.getResultList(QueryImpl.java:458)
at org.hibernate.jpa.criteria.compile.CriteriaQueryTypeQueryAdapter.getResultList(CriteriaQueryTypeQueryAdapter.java:67)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(SimpleJpaRepository.java:323)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:483)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:468)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:440)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:61)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:131)
//설정 추가
@Configuration
@ConfigurationProperties(prefix = "hikari.datasource")
public class JpaConfig extends HikariConfig {
@Bean
public DataSource dataSource() throws SQLException{
return new HikariDataSource(this);
}
}
문제는 org.springframework.mock.web에 있는 mock set들은 Servlet 3.0 API를 기반으로 동작하는데 현재 프로젝트 Servlet 버전은 2.5였다.
변경 전
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
변경 후
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
The Spring 4.0.1 reference documentation is now more clear about the Mocks: Servlet 3.0+ is strongly recommended and a prerequisite in Spring's test and mock packages for test setups in development environments.
5. 날짜
// 기존 날짜 데이터 처리 방식
long plusDay = xxxSchedule.getExecutionDate().getTime() + TimeUnit.DAYS.toMillis(extendDays);
xxxSchedule.setRegisterDate(Date.from(Instant.now()));
xxxSchedule.setExecutionDate(new Date(plugDay));
// 자바8 현재 날짜 구하기
LocalDateTime.now()
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
// 타임스탬프에서 날짜 가져오기
Long time = 1470651527000L;
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
String format = dateFormat.format(time);
@RequestParam(value = "bongsoo", required = false) int bongsoo
int형이기 때문에 값이 없을때 null일 수 없다. 그래서 defaultValue를 미리 지정해주는게 좋다.
@RequestParam(value = "bongsoo", required = false, defaultValue="0") int bongsoo
11. 세션 스코프 빈 사용
컨트롤러에서 세션 스코프 빈을 사용할 일이 있었고, 컨트롤러는 싱글톤 빈이기 때문에 일반적으로 DI방식을 이용해 주입해서는 방법이 없다. DL 방식을 이용하는 방법도 있지만 애플리케이션 로직에 스프링 코드가 들어간다는 단점이 있다.
그래서 프록시 DI 방식을 택했다.
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class Work {
...
}
@Scope 애노테이션으로 스코프를 지정했다면 proxyMode 엘리먼트를 이용해서 프록시를 이용한 DI가 되도록 지정할 수 있다. 클라이언트(여기선 컨트롤러 클래스)는 스코프 프록시 오브젝트를 실제 스코프 빈처럼 사용하면 프록시에서 현재 스코프에 맞는 실제 빈 오브젝트로 작업을 위임해준다.
스코프 프록시는 각 요청에 연결된 HTTP 세션정보를 참고해서 사용자마다 다른 Work 오브젝트를 사용하게 해준다. 클라이언트인 컨트롤러 입장에서는 모두 같은 오브젝트를 사용하는 것처럼 보이지만, 실제로는 그 뒤에 사용자별로 만들어진 여러 개의 Work가 존재하고, 스코프 프록시는 실제 Work 오브젝트로 클라이언트의 호출을 위임해주는 역할을 해줄 뿐이다.
프록시 빈이 인터페이스를 구현하고 있고, 클라이언트에서 인터페이스로 DI 받는다면 proxyMode를 ScopedProxyMode.INTERFACES로 지정해주고, 프록시 빈 클래스를 직접 DI 한다면 ScopedProxyMode.TARGET_CLASS로 지정하면 된다(여기서는 Work 클래스로 직접 DI 할 것이므로 ScopedProxyMode.TARGET_CLASS).
스코프 프록시의 DI 사용
@Controller
public void MainController {
@Autowired
Work work;
}
DI 받을 때 클래스를 이용한다면 proxy-target-class를 true로 설정하고, 인터페이스를 이용한다면 false로 하거나 아예 생략하면 된다.
12. Spring Boot war 배포로 변경하기
pom.xml
변경 전 : <packaging>jar</packaging>
변경 후 : <packaging>war</packaging>
configuration
@SpringBootApplication
public class Application extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
...
}
@Configuration
public class AppConfig extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
13. Spring Boot CORS 처리
대부분의 웹 브라우저는 보안상의 이유로 다른 도메인의 URL을 호출해서 데이터를 가져오는 것을 금지하고 있다. 우리 웹 서비스에서만 사용하기 위해 다른 서브 도메인을 가진 API 서버를 구축했는데, 다른 웹 서비스에서 마음대로 접근해서 사용하면 문제가 되기 때문이다.
그런데 하나의 도메인을 가진 웹 서버에서 모든 처리를 하기에는 효율성이나 성능 등 여러 문제로 각 기능별로 여러 서버를 두는 경우가 많다(API 서버, WAS 서버, 파일 서버 등등). 물리적으로 분리된 서버이고, 다른 용도로 구축된 서버이니 당연히 각각 다른 도메인을 가진 서버들일 텐데, 서로간에 Ajax 통신을 할 수 없는 것일까? 즉 서로 다른 도메인 간의 호출을 의미하는 크로스 도메인 문제를 해결할 수는 없는 것일까?
CORS(Cross Origin Resource Sharing)은 외부 도메인에서의 요청(접근)을 허용해주는 메커니즘이다.
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@RequestMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
컨트롤러 전체에 적용도 가능하다.
@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@RequestMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@RequestMapping(method = RequestMethod.DELETE, value = "/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
Java Config
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(false).maxAge(3600);
}
}
HandlerMethodArgumentResolver 인터페이스를 구현하여 컨트롤러의 메서드 파라미터를 검증, 수정 할 수 있다.
/**
* Strategy interface for resolving method parameters into argument values in
* the context of a given request.
*
* @author Arjen Poutsma
* @since 3.1
* @see HandlerMethodReturnValueHandler
*/
public interface HandlerMethodArgumentResolver {
/**
* Whether the given {@linkplain MethodParameter method parameter} is
* supported by this resolver.
* @param parameter the method parameter to check
* @return {@code true} if this resolver supports the supplied parameter;
* {@code false} otherwise
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Resolves a method parameter into an argument value from a given request.
* A {@link ModelAndViewContainer} provides access to the model for the
* request. A {@link WebDataBinderFactory} provides a way to create
* a {@link WebDataBinder} instance when needed for data binding and
* type conversion purposes.
* @param parameter the method parameter to resolve. This parameter must
* have previously been passed to {@link #supportsParameter} which must
* have returned {@code true}.
* @param mavContainer the ModelAndViewContainer for the current request
* @param webRequest the current request
* @param binderFactory a factory for creating {@link WebDataBinder} instances
* @return the resolved argument value, or {@code null}
* @throws Exception in case of errors with the preparation of argument values
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
쿠키로 인증 구현 예
public class CookieHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
private XxxService xxxService;
public CookieHandlerMethodArgumentResolver(XxxService xxxService) {
this.xxxService = xxxService;
}
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.hasParameterAnnotation(Authentication.class)
&& methodParameter.getParameterType().equals(User.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
javax.servlet.http.Cookie cookie = WebUtils.getCookie(servletRequest, "loginCookie");
if (StringUtils.isNotEmpty(cookie)) {
String EncryptVal = cookie.getValue();
String userId = JavaEnCrypto.Decrypt(EncryptVal);
if (isVaild(userId)) {
User user = new User();
user.setUserName(userId);
return user;
}
}
return null;
}
private boolean isVaild(String userId) {
if (xxxService.checkEmail(userId).isDuplicated()){
return true;
}
else {
return false;
}
}
}
설정은 다음과 같이 한다.
//boot config
@SpringBootApplication
public class Application extends WebMvcConfigurerAdapter {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(authenticationResolver(xxxService));
}
@Bean
public CookieHandlerMethodArgumentResolver authenticationResolver(xxxService xxxService) {
CookieHandlerMethodArgumentResolver cookieHandlerMethodArgumentResolver = new CookieHandlerMethodArgumentResolver(xxxService);
return cookieHandlerMethodArgumentResolver;
}
}
//Controller class
public ResponseEntity<ResultResponse> logout(@Authentication User user, HttpServletResponse response) {
if (user != null) {}
else {}
}
public class Profile {
public interface PublicView {}
public interface FriendsView extends PublicView {}
public interface FamilyView extends FriendsView {}
}
public class User {
@JsonView(Profile.PublicView.class)
private String userId;
private String password;
private int age;
@JsonView(Profile.FamilyView.class)
private long mobnum;
@JsonView(Profile.FriendsView.class)
private String mailId;
@JsonView(Profile.PublicView.class)
private String name;
@JsonView(Profile.PublicView.class)
private College college;
@JsonView(Profile.PublicView.class)
private Address address;
...
}
public class College {
@JsonView(Profile.PublicView.class)
private String colName;
@JsonView(Profile.FriendsView.class)
private String colLocation;
...
}
public class Address {
@JsonView(Profile.FamilyView.class)
private String houseNo;
@JsonView(Profile.FriendsView.class)
private String city;
@JsonView(Profile.PublicView.class)
private String country;
...
}
결과 /app/publicprofile
결과 app/friendprofile
결과 app/familyprofile
21. Spring Boot Embedded Tomcat AJP 연동
Spring Boot의 Embeded WAS로 Tomcat을 쓸 경우 아래와 같은 설정으로 AJP 연동을 할 수 있다.
@Bean
public EmbeddedServletContainerFactory servletContainer() {
TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
tomcat.addContextCustomizers((context) -> {
StandardRoot standardRoot = new StandardRoot(context);
standardRoot.setCacheMaxSize(100 * 1024);
standardRoot.setCacheObjectMaxSize(4 * 1024);
});
if (tomcatAjpEnabled) {
Connector ajpConnector = new Connector("AJP/1.3");
ajpConnector.setProtocol("AJP/1.3");
ajpConnector.setPort(ajpPort);
ajpConnector.setSecure(false);
ajpConnector.setAllowTrace(false);
ajpConnector.setScheme("http");
tomcat.addAdditionalTomcatConnectors(ajpConnector);
}
return tomcat;
}