2018년 2월 6일 화요일

github를 maven repository로 사용하기

공부하면서 점차 단일 프로젝트가 아닌 멀티 모듈 프로젝트로, 또는 복수의 참조 프로젝트를 구성하여 개발하면서 maven repository를 사용하고 싶은 경우가 생기게 된다.

따로 repo server를 두면 좋겠지만 개인 개발용인 경우 이는 무리가 있다.

github를 maven repository처럼 사용하면 복수의 개인 프로젝트 참조를 좀더 편하게 할 수 있다.

해당 기능을 제공해주는 https://jitpack.io/ 사이트가 있다.

jitpack을 사용하면 개인 프로젝트에 대해 쉽게 참조하여 사용할 수 있지만 이 경우 로컬 참조와 maven repository 참조를 바로바로 변환하지 못하는 단점이 있다.

jitpack은 여러모로 사용이 불편하지만 github를 maven repository로 사용하면 편하게 개인 참조 프로젝트를 사용할 수 있다.

사용 방법은 다음과 같다.
(maven repository를 github repository 에 만드는 것이다. 둘다 repository라는 단어를 쓰므로 구분해서 인식할 필요가 있다.)

  1. github에 maven repository 용 github repository 생성
    • bluesky-maven-repo 생성
  2. 해당 github repository를 local에 clon
    • local git 폴더에 bluesky-maven-repo 를 clon 받음
  3. maven deploy 할 프로젝트를 local  github repository 위치에 배포
    • bluesky-maven-repo 에 해당 jar가 배포 됨
  4. 해당 github repository를 push
  5. 사용할 프로젝트에서 해당 github repotiroy를 maven repository로 설정하여 사용

1, 2의 경우는 기존 github 사용이므로 설명은 생략한다.

3번의 maven 배포는 다음과 같은 명령어를 사용한다.


mvn deploy -DaltDeploymentRepository=snapshot-repo::default::file:../bluesky-maven-repo/snapshots

file의 위치는 bluesky-maven-repo 위치에 해당 브런치로 빌드하면 된다.

이렇게 배포된 내용을 github에 push 후 사용할 프로젝트에 다음과 같이 maven repository 설정을 추가한다.


<repositories>
 <repository>
  <id>github-bluesky-maven-repo</id>
  <url>https://raw.githubusercontent.com/[본인github]/bluesky-maven-repo/master/snapshots</url>
  <snapshots>
   <enabled>true</enabled>
   <updatePolicy>always</updatePolicy>
  </snapshots>
 </repository>
</repositories>

위 경우는 bluesky-maven-repo 라는 이름의 snapshot repository를 사용한 경우이며 이런 설정은 사용자마다 다르게 하면 된다.

이제 대상 프로젝트를 참조하여 사용한다.


<dependency>
 <groupId>[대상 프로젝트 groupId]</groupId>
 <artifactId>[대상 프로젝트 artifactId]</artifactId>
 <version>[대상 프로젝트 version]</version>
</dependency>

2018년 2월 2일 금요일

ServletRequestDataBinderUtil 만들기 - static method로 @ModelAttribute 처리하기

ValidationUtil 만들기 에서 언급된 케이스와 같이 확장된 컨트롤러에 대해 확장된 도메인을 획득하고 싶은 경우가 있다.

하지만 이미 상위 컨트롤러 메소드의 argument 는 고정되어 있는 객체이고 이를 확장한 도메인을 넘겨받는 처리를 하려면 generic을 사용할 수도 있다.

여기선 static method를 통해 획득할 수 있는 유틸을 만들어보았다.


/**
 * 요청받은 request 의 parameter를 기반으로 modelAttribute object를 호출하는 유틸.
 * @author bluesky
 *
 */
public class ServletRequestDataBinderUtil {

 public static  T getObject(String objectName, Class clazz) {
  HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

  T instantiateClass = BeanUtils.instantiateClass(clazz);
  
  ServletRequestDataBinder servletRequestDataBinder = objectName == null ? new ServletRequestDataBinder(instantiateClass) : new ServletRequestDataBinder(instantiateClass, objectName);
  servletRequestDataBinder.bind(request);
  
  @SuppressWarnings("unchecked")
  T target = (T) servletRequestDataBinder.getTarget();
  
  return target;
 }

 public static  T getObject(Class clazz) {
  return getObject(null, clazz);
 }
}

위 유틸을 사용해 modelAttribute를 획득하는 방법은 다음과 같다.

TestObject testObjectd = ServletRequestDataBinderUtil.getObject(TestObject.class)


ValidationUtil과 연계하여 만들면 아래와 같다.
/**
 * 요청받은 request 의 parameter를 기반으로 modelAttribute object를 호출하는 유틸.
 * @author bluesky
 *
 */
public class ServletRequestDataBinderUtil {

 public static  T getObject(String objectName, Class clazz, Object... validationHints) {
  HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

  T instantiateClass = BeanUtils.instantiateClass(clazz);
  
  ServletRequestDataBinder servletRequestDataBinder = objectName == null ? new ServletRequestDataBinder(instantiateClass) : new ServletRequestDataBinder(instantiateClass, objectName);
  servletRequestDataBinder.bind(request);
  
  @SuppressWarnings("unchecked")
  T target = (T) servletRequestDataBinder.getTarget();
  
  if (validationHints != null) {
   ValidationUtil.validate(target, validationHints);
  }
  
  return target;
 }

 public static  T getObject(Class clazz) {
  return getObject(null, clazz, (Object[]) null);
 }
 
 public static  T getObject(String objectName, Class clazz) {
  return getObject(objectName, clazz, (Object[]) null);
 }
 
 public static  T getObject(Class clazz, Object... validationHints) {
  return getObject(null, clazz, validationHints);
 }
}



validation 체크까지 하면 다음과 같다.
TestObject testObjectd = ServletRequestDataBinderUtil.getObject(TestObject.class, TestObjectValidate.class)

2018년 2월 1일 목요일

ValidationUtil 만들기 - static method로 @Validated 처리하기

Spring mvc를 사용하면 controller 의 method argument로 넘어오는 modelAttribute에 대한 Validated 처리를 할 수 있다.

만약 특정 controller를 공통화 하고 그 controller를 확장하여 확장된 도메인을 modelAttribute로 사용하고 싶은 경우가 발생하지만 이런 경우 validation 처리에 대해 확장 처리가 힘들다.

이럴 땐 method 내에서 validation 체크를 별도로 해야하는데 스프링이 제공하는 custom validator를 구현하는 것은 너무 개발 비용이 많이 드는 것 같아 기존 어노테이션 방식을 그대로 사용하는 형태의 유틸을 만들어 보았다.


import org.springframework.util.StringUtils;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindException;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

import lombok.Setter;
import lombok.SneakyThrows;

/**
 * annotation 기반 validation 체크를 지원하는 유틸 
 * @author bluesky
 *
 */
public class ValidationUtil {
 
 @Setter
 private static Validator validator;

 @SneakyThrows
 public static void validate(Object object, Object... validationHints) {
  BeanPropertyBindingResult beanPropertyBindingResult = new BeanPropertyBindingResult(object, StringUtils.uncapitalize(object.getClass().getSimpleName()));
  ValidationUtils.invokeValidator(validator, object, beanPropertyBindingResult, validationHints);
  if (beanPropertyBindingResult.hasErrors()) {
   throw new BindException(beanPropertyBindingResult);
  }
 }
}

위의 유틸을 사용하여 validation 체크를 하는 방법은 다음과 같다.

ValidationUtil.validate(testObject, TestObjectValidate.class);

2017년 6월 24일 토요일

java 8, Spring 에서 requestMapping method의 parameter name을 generic으로 획득하기

최종 목표는 @GetMapping으로 바인딩된 호출의 parameterName을 구하는 것이다.


springframework를 사용하기 전 우선 java 8에서 method의 parameterName을 generic으로 구할 수 있는가에 대해 알아보자.

우선 java 에서 generic으로 다음과 같이 parameter name을 구할 수 있다.


String name = String.class.getMethod("substring", int.class).getParameters()[0].getName()
System.out.println(name);

하지만 막상 획득해보면 원래 이름이 아닌 arg0, arg1, arg2 ... 와 같은 이름으로 반환된다.

java 가 컴파일 시 이름에 대해 메모리 공간을 낭비하지 않기 위해 이와 같이 통일된 값으로 변환하여 사용하기 때문이다.

이를 원래 parameter name으로 사용하고자 하는 경우 vm 옵션으로 -parameters 를 사용하면 된다.

Spring에서는 parameter name을 사용하여 외부 요청의 key 값을 매핑하고 있기 때문에 이에 대해 저장, 관리를 하고 있다.

이를 사용하기 위해 ParameterNameDiscoverer 를 제공하고 있다.

다음과 같이 사용하면 된다.


private static DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);

2017년 6월 8일 목요일

pageImpl 로 반환된 json 타입 restTemplate 호출하기

data jpa를 쓰는 경우 응답 처리의 paging 처리를 하기 위해 pageImpl로 반환하는 경우가 많다.
이 결과를 restTemplate으로 호출하는 경우 pageImpl 으로 반환하는 방법은 다음과 같다.
pageImpl을 재선언하여 restTemplate 호출을 받을 수 있는 class를 만든다.
아래는 단순 map 형태로 반환 받는 pageImpl의 구현 예이다.
@JsonIgnoreProperties(ignoreUnknown = true)
public class RestPageImpl extends PageImpl<Map<?, ?>> {
 private static final long serialVersionUID = 1L;

 @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
 public RestPageImpl(@JsonProperty("content") List<Map<?, ?>> content, @JsonProperty("number") int page,
   @JsonProperty("size") int size, @JsonProperty("totalElements") long total) {
  super(content, new PageRequest(page, size), total);
 }

 public RestPageImpl(List<Map<?, ?>> content, Pageable pageable, long total) {
  super(content, pageable, total);
 }

 public RestPageImpl(List> content) {
  super(content);
 }

 @SuppressWarnings("unchecked")
 public RestPageImpl() {
  super(Collections.EMPTY_LIST);
 }
}

2016년 11월 27일 일요일

ModelAttribute에 대해 annotation 적용하기

이전에 [annotation을 사용한 controller aop 설정하기] 란 글을 쓴 적이 있다.

class 나 method 단위에서 aop를 적용하는 방법에 대한 내용인데 parameter 단위로 aop를 적용을 할 수도 있다.

하지만 개인적으로 느끼기엔 parameter 단위의 aop적용은 그리 효율적이지 못하다.

aop 문법이 특정 타입에 대해 적용되어 있는 annotation을 적용하는 것에 대해 효과적인 명시 방법이 없다.

method parameter의 첫번째가 특정 조건일때, 끝이 특정 족건일 때, 중간에 특정 조건일 때 처럼만 선언이 가능하여 어색하고 딱히 확실한 명시 방법이 없다.
(혹 내가 못찾은 것 일 수도 있겠지만..)


또 하나의 문제는 spring의 ModelAttribute처럼 request parameter나 path variable에 대해 바인딩을 하려면 aop를 통한 처리는 복잡하고 어렵다.


이에 대해 Spring은 HandlerMethodArgumentResolver 기능을 제공한다.

넘어오는 매개 변수에 대해 supportsParameter 인 경우 응답값을 resolveArgument 메소드를 통해 전달해 줄 수 있다.


하지만 해당 구현만 하면 2가지 단점이 있다.
  1. 외부에서 넘어온 request parmaeter나 path variable의 값이 object 에 맵핑되지 않는다. (modelAttribute 기능이 적용되지 않는다.)
  2. hibernate validation 체크가 무시 된다.


따라서 스프링이 modelAttribute에 대해 어떻게 동작하는지를 살펴보는 것이 좋다.

스프링에서는 HandlerMethodArgumentResolver을 modelAttribute에 대해서 구현한 ModelAttributeMethodProcessor를 제공하며 특히 servlet을 사용하는 경우 ModelAttributeMethodProcessor를 확장한 ServletModelAttributeMethodProcessor를 제공한다.

ModelAttributeMethodProcessor는 resolveArgument 메소드가 final 선언 되어 있어 커스텀한 확장을 하지 못한다.


따라서 다음처럼 custom ModelAttributeMethodProcessor를 만들어준다.

public abstract class BlueskyHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
 
 /* (s) ServletModelAttributeMethodProcessor method copy */
 @Override
 public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
  String name = ModelFactory.getNameForParameter(parameter);
  Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
    createAttribute(name, parameter, binderFactory, webRequest));

  if (!mavContainer.isBindingDisabled(name)) {
   ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
   if (ann != null && !ann.binding()) {
    mavContainer.setBindingDisabled(name);
   }
  }

  WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
  if (binder.getTarget() != null) {
   if (!mavContainer.isBindingDisabled(name)) {
    bindRequestParameters(binder, webRequest);
   }
   validateIfApplicable(binder, parameter);
   if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
    throw new BindException(binder.getBindingResult());
   }
  }

  // Add resolved attribute and BindingResult at the end of the model
  Map<string, object> bindingResultModel = binder.getBindingResult().getModel();
  mavContainer.removeAttributes(bindingResultModel);
  mavContainer.addAllAttributes(bindingResultModel);

  return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
 }

 protected Object createAttribute(String attributeName, MethodParameter methodParam,
   WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {

  return BeanUtils.instantiateClass(methodParam.getParameterType());
 }
 
 protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
  ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
  ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
  servletBinder.bind(servletRequest);
 }
 
 protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
  Annotation[] annotations = methodParam.getParameterAnnotations();
  for (Annotation ann : annotations) {
   Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
   if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
    Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
    Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
    binder.validate(validationHints);
    break;
   }
  }
 }
 
 protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
  int i = methodParam.getParameterIndex();
  Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
  boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
  return !hasBindingResult;
 }
 /* (e) ServletModelAttributeMethodProcessor method copy */
}


이제 custom한 annotation을 만들어 본다.


/**
 * 로그인한 유저의 Blog 정보를 주입하기 위해 사용하는 aop
 * @author bluesky
 *
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface UserBlog {

 public boolean checkBlog() default false; // 외부 접근 파라메터에 대해 유저의 blog 인지 확인 여부
}

해당 어노테이션이 적용된 modelAttribute에 대한 methodArgumentResolver를 만들어준다.

/**
 * 유저의 Blog 정보를 반환하는 Resolver
 * @author bluesky
 *
 */
public class UserBlogHandlerMethodArgumentResolver extends BlueskyHandlerMethodArgumentResolver {

 @Override
 public boolean supportsParameter(MethodParameter parameter) {
  return Blog.class.isAssignableFrom(parameter.getParameterType()) && Arrays.asList(parameter.getParameterAnnotations()).stream().anyMatch(annotation -> annotation.annotationType().isAssignableFrom(UserBlog.class));
 }

 @Override
 public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
  List<Blog> userBlogList = BlogRequestAttributeUtil.getUserBlogList();
  if (userBlogList.isEmpty()) {
   throw new BlueskyException(BlogErrorCode.NOT_EXIST_BLOG);
  }
  
  Blog requestBlog = (Blog) super.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
  
  UserBlog userBlog = (UserBlog) Arrays.asList(parameter.getParameterAnnotations()).stream().filter(annotation -> annotation.annotationType().isAssignableFrom(UserBlog.class)).findAny().get();
  if (userBlog.checkBlog()) {
   
   if (requestBlog.getId() == 0) {
    throw new BlueskyException(BlogErrorCode.NOT_EXIST_PARAMETER_BLOG_ID);
   }
   return userBlogList.stream().filter(blog -> blog.getId() == requestBlog.getId()).findAny().orElseThrow(() -> new BlueskyException(BlogErrorCode.INVALID_PARAMETER_BLOG_ID)); // 유저의 블로그가 아닌 접근인 경우 에러
  }
  
  return userBlogList.get(0);
 }
 
}


이제 해당 annotation 을 사용하면 modelAttribute가 동작하고 hibernate validation도 처리가 되는 custom methodArgumentResolver가 동작하는 것을 확인 할 수 있다.
 @PreAuthorize(AuthorizeRole.PRE_AUTHORIZE_ROLE)
 @GetMapping(value = "/{id}/article/write")
 public String writePage(@UserBlog(checkBlog = true) Blog blog, ModelMap modelMap) {
  modelMap.addAttribute(articleCategoryService.findByBlog(blog));
  return "blog/write";
 }

2016년 11월 19일 토요일

Spring Boot 1.4.x 에서 Thymeleaf 3 사용하기

Spring boot 1.4.0.RELEASE 이후 Thymeleaf 3.0을 지원하게 되었다.

기존 프로젝트를 판올림하면서 기록해본다.

1.4.x 이후에도 Spring Boot는 Thymeleaf 2에 대해 default 선언을 하고 있다.

Spring Boot Docs에는 다음과 같이 Thymeleaf 3 적용방법을 안내하고 있다.

[Use Thymeleaf 3]


이 내용은 기본적으로 사용하는 Thymeleaf 3이 적용에 대해서 안내하고 있으며 추가적인 라이브러리 들에 대해서는 언급되어 있지 않다.

다음과 같이 pom.xml에 추가한다.

<properties>
    <thymeleaf.version>3.0.0.RELEASE</thymeleaf.version>
    <thymeleaf-layout-dialect.version>2.0.0</thymeleaf-layout-dialect.version>
</dependency>

기본으로 사용하는 thymeleaf-layout-direct의 경우 boot dependencies에는 1.x대 (thymeleaf 2이하 지원)으로 선언되어 있으나 위 선언을 하면 2.x대 (thymeleaf 3 지원)으로 버전 상속된다.

추가적으로 사용하는 라이브러리에 대해 다음가 같이 추가해주어야 한다.

Java 8을 사용하는 경우 thymeleaf-extras-java8time 을 선언해서 사용해야 하며

Spring Security를 사용하는 경우 thymeleaf-extras-springsecurity4 를 선언해야한다.
(둘다 기본 설정이 Thymeleaf 2 이하로 선언되어 있다.)

다음과 같이 추가한다.

<properties>
    <thymeleaf.version>3.0.0.RELEASE</thymeleaf.version>
    <thymeleaf-layout-dialect.version>2.0.0</thymeleaf-layout-dialect.version>
    <thymeleaf-extras-springsecurity4.version>3.0.1.RELEASE</thymeleaf-extras-springsecurity4.version>
    <thymeleaf-extras-java8time.version>3.0.0.RELEASE</thymeleaf-extras-java8time.version>
</dependency>


thymeleaf 3에서는 기존과 다르게 HTML, HTML5에 대해 모드 구분이 필요없어졌다. 하지만 spring은 아직 기본으로 HTML5를 설정하고 있어 이에 대한 warning을 제거하려면 다음과 같이 application.properties에 선언해준다. (꼭 해야할 필요는 없다.)
spring.thymeleaf.mode=HTML