@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 애노테이션 클래스를 찾는다).
메이븐 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 statementat 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)
문제는 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(newDate(plugDay));// 자바8 현재 날짜 구하기LocalDateTime.now()LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));// 타임스탬프에서 날짜 가져오기Long time =1470651527000L;DateFormat dateFormat =newSimpleDateFormat("yyyy-MM-dd");String format =dateFormat.format(time);
@Scope 애노테이션으로 스코프를 지정했다면 proxyMode 엘리먼트를 이용해서 프록시를 이용한 DI가 되도록 지정할 수 있다. 클라이언트(여기선 컨트롤러 클래스)는 스코프 프록시 오브젝트를 실제 스코프 빈처럼 사용하면 프록시에서 현재 스코프에 맞는 실제 빈 오브젝트로 작업을 위임해준다.
스코프 프록시는 각 요청에 연결된 HTTP 세션정보를 참고해서 사용자마다 다른 Work 오브젝트를 사용하게 해준다. 클라이언트인 컨트롤러 입장에서는 모두 같은 오브젝트를 사용하는 것처럼 보이지만, 실제로는 그 뒤에 사용자별로 만들어진 여러 개의 Work가 존재하고, 스코프 프록시는 실제 Work 오브젝트로 클라이언트의 호출을 위임해주는 역할을 해줄 뿐이다.
프록시 빈이 인터페이스를 구현하고 있고, 클라이언트에서 인터페이스로 DI 받는다면 proxyMode를 ScopedProxyMode.INTERFACES로 지정해주고, 프록시 빈 클래스를 직접 DI 한다면 ScopedProxyMode.TARGET_CLASS로 지정하면 된다(여기서는 Work 클래스로 직접 DI 할 것이므로 ScopedProxyMode.TARGET_CLASS).
스코프 프록시의 DI 사용@Controllerpublicvoid MainController { @AutowiredWork work;}
대부분의 웹 브라우저는 보안상의 이유로 다른 도메인의 URL을 호출해서 데이터를 가져오는 것을 금지하고 있다. 우리 웹 서비스에서만 사용하기 위해 다른 서브 도메인을 가진 API 서버를 구축했는데, 다른 웹 서비스에서 마음대로 접근해서 사용하면 문제가 되기 때문이다.
그런데 하나의 도메인을 가진 웹 서버에서 모든 처리를 하기에는 효율성이나 성능 등 여러 문제로 각 기능별로 여러 서버를 두는 경우가 많다(API 서버, WAS 서버, 파일 서버 등등). 물리적으로 분리된 서버이고, 다른 용도로 구축된 서버이니 당연히 각각 다른 도메인을 가진 서버들일 텐데, 서로간에 Ajax 통신을 할 수 없는 것일까? 즉 서로 다른 도메인 간의 호출을 의미하는 크로스 도메인 문제를 해결할 수는 없는 것일까?
CORS(Cross Origin Resource Sharing)은 외부 도메인에서의 요청(접근)을 허용해주는 메커니즘이다.
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 */publicinterfaceHandlerMethodArgumentResolver { /** * 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 */booleansupportsParameter(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} * @throwsException in case of errors with the preparation of argument values */ObjectresolveArgument(MethodParameter parameter,ModelAndViewContainer mavContainer,NativeWebRequest webRequest,WebDataBinderFactory binderFactory) throwsException;}
쿠키로 인증 구현 예
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;
}