2013年6月14日金曜日

【備忘録】spring + aspectj によるAOP

AOP (Aspect Oriented Programming) はある特定の振る舞い(aspect)を分離し、既存の振る舞いに対し入れ込むような時に利用される。例えば以下のようなものが挙げられます。

  • 特定処理にログを入れる
  • 開始終了の処理時間を計測する
  • 特定処理の呼び出し回数を計測する
今回は、spring mvc を使ってある特定の controller の処理結果をハンドリングし、例外が発生した場合、自動的にエラーオブジェクトを生成して結果を返すような処理を作ってみたので、備忘録的にまとめておきます。

controller は以下、入力を受けて service の処理結果をjsonとして返却します。
@Controller
public class HelloController {
  
  private static Logger logger = LoggerFactory.getLogger(HelloController.class);
  
  @Autowired
  private HelloService service;
  
  @Procedure
  @RequestMapping(value="/hello/{type}", method={RequestMethod.GET})
  public @ResponseBody
  Output<?> hello(
      @PathVariable("type") String type) throws Exception {
    logger.info("- start hello");
    try {
    if ("exception".equalsIgnoreCase(type))
      return new Output<Hello>(UUID.randomUUID().toString(), service.runOnException());
    else if ("failure".equalsIgnoreCase(type)) 
      return new Output<Hello>(UUID.randomUUID().toString(), service.runOnFailure());
    
    return new Output<Hello>(UUID.randomUUID().toString(), service.runOnSuccess());
    } finally {
      logger.info("- end hello");
    }
  }
}

output は以下のようなpojoです。
public class Output<T> {
  
  private String trxId;
  private int statusCode;
  private String statusMessage;
  private T data;

  public Output( String trxId, int statusCode, String statusMessage) {
    this.trxId = trxId;
    this.statusCode = statusCode;
    this.statusMessage = "";
  }
  
  public Output( String trxId, T data) {
    this.trxId = trxId;
    this.statusCode = 200;
    this.data = data;
    this.statusMessage = "";
  }
  
  public String getTrxId() {
    return trxId;
  }

  public void setTrxId(String trxId) {
    this.trxId = trxId;
  }

  public int getStatusCode() {
    return statusCode;
  }

  public void setStatusCode(int statusCode) {
    this.statusCode = statusCode;
  }

  public String getStatusMessage() {
    return statusMessage;
  }

  public void setStatusMessage(String statusMessage) {
    this.statusMessage = statusMessage;
  }

  public T getData() {
    return data;
  }

  public void setData(T data) {
    this.data = data;
  }
  
  public String toString() {
    return new StringBuilder().append("@").append(this.trxId).append("-[")
        .append(this.statusCode).append(": ")
        .append(this.statusMessage).append("], ").append(data)
        .toString();
  }
}

で、AOP を利用しない場合、呼び出し結果は以下のようになります。
10:52:40.811 [http-8080-2] INFO  j.b.h.example.aop.HelloController - - start hello
10:52:40.845 [http-8080-2] INFO  j.b.h.example.aop.HelloServiceImpl - success
10:52:40.845 [http-8080-2] INFO  j.b.h.example.aop.HelloController - - end hello

このままでは例外発生時にエラー情報を返却できないです。
なので、hello() に対し、@Procedure というアノテーションを付けて、@Procedure がついた処理について例外が発生した場合、エラーのOutputを生成しレスポンスしたいと考えてみました。
AOP は AspectJ を利用します。Spring+AspectJ はこちらを参考に
@Component
@Aspect
public class ProcedureAspect {

  static Logger logger = LoggerFactory.getLogger(ProcedureAspect.class);

  @Pointcut("execution(* jp.blogspot.horiga3.*.*(..)) ")
  public void targetMethods() {}
  
  @Before("@annotation(jp.blogspot.horiga3.example.aop.Procedure)")
  public void preHandle() {
    logger.info("Aspect :: preHandle");
  }
  
  @AfterReturning(
      pointcut="@annotation(jp.blogspot.horiga3.example.aop.Procedure)",
      returning="retVal")
  public void postHandle(Object retVal) {
    logger.info("Aspect :: postHandle, retVal={}", retVal != null ? retVal.toString() : "null");
  }
  
  @Around("@annotation(jp.blogspot.horiga3.example.aop.Procedure)")
  public Object handle(ProceedingJoinPoint pjp) {

    logger.info("Aspect :: around - start");

    Object[] args;
    try {
      args = pjp.getArgs();
      return args == null ? pjp.proceed() : pjp.proceed(args);
    } catch (Throwable e) {
      logger.info("Aspect :: handleException");
      int statusCode = 500;
      String statusMessage = "unknown";
      if (e instanceof ProcedureException) {
        statusCode = ((ProcedureException) e).getStatusCode();
        statusMessage = ((ProcedureException) e).getStatusMessage();
      } else if (e instanceof IllegalArgumentException) {
        statusCode = 400;
        statusMessage = "Invalid parameter";
      }
      Output<Object> error = new Output<Object>(UUID.randomUUID().toString(), statusCode, statusMessage);
      return error;
    } finally {
      logger.info("Aspect :: around - end");
    }
  }
}

spring の設定は、aop:aspectj-autoproxy を追加しただけ
<?xml version="1.0" encoding="UTF-8"?>
<beans 
  xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:aop="http://www.springframework.org/schema/aop" 
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation=
     "http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" >
  
  <mvc:annotation-driven />

  <aop:aspectj-autoproxy/>
  <context:component-scan base-package="jp.blogspot.horiga3.example.aop"/>
  
</beans>

で、実行すると例外の場合もjsonを返却するようになりました。既存のControllerは何も修正していないですね。
10:52:40.768 [http-8080-2] INFO  j.b.h.example.aop.ProcedureAspect - Aspect :: around - start
10:52:40.772 [http-8080-2] INFO  j.b.h.example.aop.ProcedureAspect - Aspect :: preHandle
10:52:40.811 [http-8080-2] INFO  j.b.h.example.aop.HelloController - - start hello
10:52:40.845 [http-8080-2] INFO  j.b.h.example.aop.HelloServiceImpl - success
10:52:40.845 [http-8080-2] INFO  j.b.h.example.aop.HelloController - - end hello
10:52:40.845 [http-8080-2] INFO  j.b.h.example.aop.ProcedureAspect - Aspect :: around - end
10:52:40.845 [http-8080-2] INFO  j.b.h.example.aop.ProcedureAspect - Aspect :: postHandle, retVal=@11d8c89a-6fc2-4e38-a745-f2ade9c3d6ff-[200: ], jp.blogspot.horiga3.example.aop.Hello@6dbf4a72

@AfterReturningが @Around より後に来ることは予想できたけど、@Around が @Before より先に来るんですね。

2013年6月8日土曜日

springframework を利用した JavaMail 送信 の覚え書き

springframework を利用して Mail を送信することがあったので、覚え書き。spring は何かと設定を xml にする必要があって覚えるのが大変と思って Guice をここ2年程度利用していたが、最近は annotation でほぼ設定できるようになってて結構良い感じだった。まぁ、springframework はそれだけではなく巨大なフレームワークだからいろいろと機能が沢山あって全てを語るには勉強が足りないですww。

個人的に最近気になっているのは、playframework と、vertx かなと JVM + netty をベースにした framework が良い性能をだしていて少しづつ勉強しているところです。

話がずれたので、とりあえず覚え書き。

まず、spring の applicationContext.xml で設定する bean 設定。mail 設定の部分だけ別ファイルとして設定
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
 http://www.springframework.org/schema/beans/spring-beans.xsd">
 
 <bean id="mailSender" 
  class="org.springframework.mail.javamail.JavaMailSenderImpl">
  <property name="host" value="${smtp.host}"/>
 </bean>
 
 <bean id="velocityEngine" class="org.springframework.ui.velocity.VelocityEngineFactoryBean">
  <property name="resourceLoaderPath" value="classpath:mail" />
  <property name="velocityPropertiesMap">
   <map>
                <entry key="input.encoding" value="UTF-8" />
                <entry key="output.encoding" value="UTF-8" />
            </map>
  </property>
 </bean>
 
 <bean id="velocityJavaMailSender" class="jp.blogspot.horiga3.example.spring.mail.VelocityJavaMailSender">
  <property name="mailSender" ref="mailSender" />
  <property name="velocityEngine" ref="velocityEngine" />
 </bean>
</beans>

SMTPサーバはないと動きません。幸いにも社内にあるのでそれを設定します。それぞれの環境に合わせて変更するひつようがありますのであしからず。

あとは、自前クラスは以下のように
import javax.mail.internet.MimeMessage;

import org.apache.velocity.app.VelocityEngine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Component;
import org.springframework.ui.velocity.VelocityEngineUtils;

@Component
public class VelocityJavaMailSender {

 @Autowired
 protected JavaMailSender mailSender;
 
 @Autowired
 protected VelocityEngine velocityEngine;

 public static class MailMessage {
  
  private boolean html = false;
  
  private String from;
  private String personal;
  private String mailTemplate;
  private String[] recipients;
  private String subject;
  private Map<String, Object> content;

  public boolean isHtml() {
   return html;
  }

  public void setHtml(boolean html) {
   this.html = html;
  }

  public String getFrom() {
   return from;
  }

  public void setFrom(String from) {
   this.from = from;
  }

  public String getPersonal() {
   return personal;
  }

  public void setPersonal(String personal) {
   this.personal = personal;
  }

  public String getMailTemplate() {
   return mailTemplate;
  }

  public void setMailTemplate(String mailTemplate) {
   this.mailTemplate = mailTemplate;
  }

  public String[] getRecipients() {
   return recipients;
  }

  public void setRecipients(String[] recipients) {
   this.recipients = recipients;
  }

  public String getSubject() {
   return subject;
  }

  public void setSubject(String subject) {
   this.subject = subject;
  }

  public Map<String, Object> getContent() {
   return content;
  }

  public void setContent(Map<String, Object> content) {
   this.content = content;
  }
 }
 
 public void sendMailMessage(MailMessage mail) throws Exception {
  this.mailSender.send(createMailPreparator(mail));
 }
 
 public void setMailSender(JavaMailSender mailSender) {
  this.mailSender = mailSender;
 }
 
 public void setVelocityEngine(VelocityEngine velocityEngine) {
  this.velocityEngine = velocityEngine;
 }
 
 private MimeMessagePreparator createMailPreparator(
   final MailMessage mailMessage) {
  MimeMessagePreparator preparator = new MimeMessagePreparator() {
   @Override
   public void prepare(MimeMessage mimeMessage) throws Exception {
    MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
    message.setTo(mailMessage.getRecipients());
    message.setSubject(mailMessage.getSubject());
    if ( null != mailMessage.getPersonal() && mailMessage.getPersonal().trim().length() > 0)
     message.setFrom(mailMessage.getFrom(), mailMessage.getPersonal());
    else message.setFrom(mailMessage.getFrom());
    message.setText(VelocityEngineUtils.mergeTemplateIntoString(
      velocityEngine, mailMessage.getMailTemplate(), "utf-8",
      mailMessage.getContent()), mailMessage.isHtml());
   }
  };
  return preparator;
 }
}


で、テストケースが以下。
import java.util.HashMap;

import junit.framework.Assert;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import jp.blogspot.horiga3.example.spring.mail.VelocityJavaMailSender.MailMessage;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={
 "file:src/main/webapp/WEB-INF/springframework/mail-context.xml"
})
public class VelocityJavaMailSenderTest {
 
 @Autowired
 @Qualifier("velocityJavaMailSender")
 VelocityJavaMailSender mailSender;
 
 @Test
 public void test() {
  
  MailMessage message = new MailMessage();
  
  try {
   message.setMailTemplate("test.vm");
   message.setRecipients(new String[] { "your mail address " });
   message.setSubject("test message");
   message.setFrom("noreply@hogehoge.com");
   message.setPersonal("JUnitさん");
   HashMap<String, Object> content = new HashMap<String, Object>();
   content.put("str", "test");
   content.put("n", System.currentTimeMillis()/1000);
   message.setContent(content);
   mailSender.sendMailMessage(message);
  } catch ( Exception e) {
   e.printStackTrace();
   Assert.fail(e.getMessage());
  }
 }
}

送信先を自分のメールアドレスに設定してテストケースを実行すると
正しく送信されていることが確認できた。
ちなみに少しハマったところが、テンプレートファイルのエンコーディングとかも全て統一しておかないと文字化けします。
テストケースにこのソースがあるとmavenビルドとかで、自動テストするとビルドのたびに毎回メールくるようになります。注意しましょう〜