출처: http://pann.nate.com/talk/322264103?listType=c&page=1

0416, 잊지말자.

'대한민국' 이라는 이름의 배가 무너진 날을.

깔끔하게 보고 싶으면 아래 링크에서 보라.
https://gist.github.com/ihoneymon/56dd964336322eea04dc


19:04:00 ERROR c.i.i.s.s.system.MailServiceImpl - >> Occur Exception: Failed messages: com.sun.mail.smtp.SMTPSendFailedException: 530 5.7.0 Must issue a STARTTLS command first. a11sm6769399pdj.54 - gsmtp

위의 메시지가 나타난다면, compile "com.sun.mail:javax.mail" 의존성을 추가하자.

○ build.gradle

dependencies {
  //.. 중략
  compile "org.springframework:spring-context-support"
  /**
   * @see http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-email
   *
   * Javax 관련 의존성: com.sun.mail:javax.mail 과 javax.mail:javax.mail-api 두개만 존재한다.
   * @see https://github.com/spring-projects/spring-boot/blob/master/spring-boot-dependencies/pom.xml
   * 를 살펴보면 javax.mail:mail 관련한 의존성이 빠져있는 것을 볼 수 있다. ㅡ_-)> 흠...
   */
  compile "com.sun.mail:javax.mail"
  //.. 중략
}

spring-context-support, javax.mail 에 대한 의존성을 추가한다.

○ properties.yml

mail:
  host: smtp.gmail.com
  port: 587
  protocol: smtp
  default-encoding: UTF-8
  username: your-email@domain
  password: password
  smtp:
    start-tls-enable: true
    auth: true

○ MailConfig

메일발송에 사용할 템플릿엔진으로는 Thymeleaf(http://www.thymeleaf.org/doc/articles/springmail.html)를 참고하여 작성하였다. 기존에 사용하던 메일설정을 활용했다.

import java.util.Properties;

import javax.validation.constraints.NotNull;

import lombok.Data;
import lombok.ToString;

import org.hibernate.validator.constraints.NotBlank;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.TemplateResolver;

/**
 *
 * @author jiheon
 *
 * @see <a
 *      href="http://www.thymeleaf.org/doc/articles/springmail.html">http://www.thymeleaf.org/doc/articles/springmail.html</a>
 * @see <a
 *      href="http://stackoverflow.com/questions/25610281/spring-boot-sending-emails-using-thymeleaf-as-template-configuration-does-not">http://stackoverflow.com/questions/25610281/spring-boot-sending-emails-using-thymeleaf-as-template-configuration-does-not</a>
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "mail", locations = { "classpath:properties/properties.yml" })
@ToString
public class MailConfig {
    private static final String MAIL_DEBUG = "mail.debug";
    private static final String MAIL_SMTP_STARTTLS_REQUIRED = "mail.smtp.starttls.required";
    private static final String MAIL_SMTP_AUTH = "mail.smtp.auth";
    private static final String MAIL_SMTP_STARTTLS_ENABLE = "mail.smtp.starttls.enable";

    @Data
    public static class Smtp {
        private boolean auth;
        private boolean startTlsRequired;
        private boolean startTlsEnable;
    }

    @NotBlank
    private String host;
    private String protocol;
    private int port;
    private String username;
    private String password;
    private String defaultEncoding;
    @NotNull
    private Smtp smtp;

    @Bean
    public JavaMailSender mailSender() {
        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
        mailSender.setHost(getHost());
        mailSender.setProtocol(getProtocol());
        mailSender.setPort(getPort());
        mailSender.setUsername(getUsername());
        mailSender.setPassword(getPassword());
        mailSender.setDefaultEncoding(getDefaultEncoding());
        Properties properties = mailSender.getJavaMailProperties();
        properties.put(MAIL_SMTP_STARTTLS_REQUIRED, getSmtp().isStartTlsRequired());
        properties.put(MAIL_SMTP_STARTTLS_ENABLE, getSmtp().isStartTlsEnable());
        properties.put(MAIL_SMTP_AUTH, getSmtp().isAuth());
        properties.put(MAIL_DEBUG, true);
        mailSender.setJavaMailProperties(properties);
        return mailSender;
    }

    @Bean
    public TemplateResolver emailTemplateResolver() {
        TemplateResolver emailTemplateResolver = new ClassLoaderTemplateResolver();
        emailTemplateResolver.setPrefix("mails/");
        emailTemplateResolver.setSuffix(".html");
        emailTemplateResolver.setTemplateMode("HTML5");
        emailTemplateResolver.setCharacterEncoding("UTF-8");
        emailTemplateResolver.setCacheable(true);
        return emailTemplateResolver;
    }
}

○ MailServiceTest

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@ActiveProfiles("test")
@WebAppConfiguration
public class MailServiceTest {

    @Autowired
    private MailService mailService;
    @Test
    public void test() {
        ThymeleafMailMessage mailMessage = new ThymeleafMailMessage("test");
        mailMessage.setFrom("test-email@domain");
        mailMessage.setTo("test-email@domain");
        mailMessage.setSubject("[Test] mailTest");
        mailMessage.addAttribute("name", "Guest");
        mailMessage.addAttribute("imageResourceName", "imageResourceName");
        mailMessage.setEncoding("UTF-8");

        mailService.send(mailMessage);
    }

}

○ MailMessage 클래스들

● MailMessage

import java.io.File;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

import org.springframework.mail.SimpleMailMessage;

import com.google.common.collect.Lists;

@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MailMessage extends SimpleMailMessage {
    private static final long serialVersionUID = 1830106734321133565L;

    private List<File> attachments;
    private String encoding;
    private boolean htmlContent;

    public MailMessage() {
        super();
        super.setSentDate(Calendar.getInstance().getTime());
        this.attachments = Lists.newArrayList();
    }

    public MailMessage addAttachment(File file) {
        if (null != file) {
            this.attachments.add(file);
        }
        return this;
    }

    public MailMessage removeAttachment(File file) {
        if (null != file && this.attachments.contains(file)) {
            this.attachments.remove(file);
        }
        return this;
    }

    public List<File> getAttachments() {
        return Collections.unmodifiableList(this.attachments);
    }

    public boolean isMultipart() {
        return !this.attachments.isEmpty();
    }
}

● ThymeleafMailMessage

import java.util.Map;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;

import com.google.common.collect.Maps;

/**
 * Thymeleaf MailMessage
 *
 * @author jiheon
 *
 */
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ThymeleafMailMessage extends MailMessage {
    private static final long serialVersionUID = -2313892947287620959L;
    @Getter
    private final String templateName;
    private final Map<String, Object> attributes;

    public ThymeleafMailMessage(String templateName) {
        this.templateName = templateName;
        this.attributes = Maps.newHashMap();
        super.setHtmlContent(true);
    }

    public ThymeleafMailMessage addAttribute(String key, Object value) {
        this.attributes.put(key, value);
        return this;
    }

    public ThymeleafMailMessage removeAttribute(String key) {
        if (this.attributes.containsKey(key)) {
            this.attributes.remove(key);
        }
        return this;
    }

    public Map<String, Object> getAttributes() {
        return java.util.Collections.unmodifiableMap(this.attributes);
    }
}

○ MailService

● interface MailService

import java.util.List;

import org.springframework.mail.MailException;

/**
 * MailService
 *
 * @author jiheon
 *
 */
public interface MailService {

    void send(MailMessage message) throws MailException;

    void send(List<MailMessage> messages) throws MailException;
}

● class MailServiceImpl

import java.util.List;
import java.util.Locale;

import javax.annotation.PostConstruct;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.MimeMessage;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@Slf4j
@Service
public class MailServiceImpl implements MailService {
    @Autowired
    private JavaMailSender mailSender;
    @Autowired
    private SystemMailService systemMailService;
    @Autowired
    private TemplateEngine templateEngine;

    @PostConstruct
    public void setUp() {
        if (systemMailService.hasSystemMailConfig()) {
            mailSender = systemMailService.getMailSender();
        }
        log.debug("MailSender: {}", mailSender);
    }

    @Override
    public void send(MailMessage message) throws MailException {
        sendMail(message);
    }

    private void sendMail(MailMessage message) {
        try {
            log.debug(">> Send mailMessage: {}", message);
            if (ThymeleafMailMessage.class.isAssignableFrom(message.getClass())) {
                doThymeleafMailMessageSend((ThymeleafMailMessage) message);
            } else {
                doSend(message);
            }
        } catch (Exception e) {
            log.error(">> Occur Exception: {}", e.getMessage());
        }
    }

    private void doThymeleafMailMessageSend(ThymeleafMailMessage thymeleafMailMessage) throws MessagingException {
        Context context = new Context(Locale.getDefault());
        context.setVariables(thymeleafMailMessage.getAttributes());
        thymeleafMailMessage.setText(templateEngine.process(thymeleafMailMessage.getTemplateName(), context));

        doSend(thymeleafMailMessage);
    }

    private void doSend(MailMessage message) throws MessagingException {
        try {
            MimeMessage mimeMessage = new MimeMessage(Session.getInstance(System.getProperties()));
            MimeMessageHelper helper = getMimeMessageHelper(message, mimeMessage);
            mailSender.send(helper.getMimeMessage());
        } catch (MessagingException e) {
            throw e;
        }
    }

    @Override
    public void send(List<MailMessage> messages) throws MailException {
        for (MailMessage mailMessage : messages) {
            sendMail(mailMessage);
        }
    }

    private MimeMessageHelper getMimeMessageHelper(MailMessage message, MimeMessage mimeMessage)
            throws MessagingException {
        MimeMessageHelper mimeMessageHelper = makeMessageHelper(message, mimeMessage);
        mimeMessageHelper.setFrom(message.getFrom());
        mimeMessageHelper.setTo(message.getTo());
        if (null != message.getCc()) {
            mimeMessageHelper.setCc(message.getCc());
        }
        if (null != message.getBcc()) {
            mimeMessageHelper.setBcc(message.getBcc());
        }

        mimeMessageHelper.setSubject(message.getSubject());
        mimeMessageHelper.setText(message.getText(), message.isHtmlContent());
        mimeMessageHelper.setSentDate(message.getSentDate());
        return mimeMessageHelper;
    }

    private MimeMessageHelper makeMessageHelper(MailMessage message, MimeMessage mimeMessage) throws MessagingException {
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, message.isMultipart());
        if (StringUtils.hasText(message.getEncoding())) {
            helper = new MimeMessageHelper(mimeMessage, message.isMultipart(), message.getEncoding());
        }
        return helper;
    }

}

○ 정리

  • compile "com.sun.mail:javax.mail" 을 선언하지 않고 테스트를 돌려보면

    19:04:00 ERROR c.i.i.s.s.system.MailServiceImpl - >> Occur Exception: Failed messages: com.sun.mail.smtp.SMTPSendFailedException: 530 5.7.0 Must issue a STARTTLS command first. a11sm6769399pdj.54 - gsmtp

다음과 같은 메시지를 볼 수가 있다. ㅡ_-);; 이것 땜시 반나절을 삽질을 해버렸네... 두둥...



스프링부트만 믿고 개발에 정진해왔다. 그러다가 시연해야할 떄가 되어 젠킨스를 통해서 개발서버에 빌드-배포된 war 파일을 실행하여 테스트하려고 하니 Thymeleaf template engine에서 template file 을 찾지못하는 문제가 발생했다.

이 문제가 왜 생기는지를 고민하고 검색해봐도 별다른 내용을 찾을 수가 없었다.

스프링부트 튜토리얼을 살펴보다가, 컨트롤러 영역에서 조금 다른 차이점을 찾아냈다.

@Controller
public class TestController {

    @RequestMapping("/test")
    public String test() {
        return "test";
    }
}

문제가 발생하는 화면의 컨트롤러에서는

@Controller
public class TestController {

    @RequestMapping("/test")
    public String test() {
        return "/test";
    }
}

와 같은 형태로 정의가 되어 있는 것이다.

차이를 발견했는가???

저 문제를 찾지 못해서 헤매였다. 1시간여를…. 흙… Orz…

해결방법

return "/test";return "test"; 으로 정의하면 된다.
bootRepackage로 빌드된 애플리케이션 배포본은 경로에 민감하게 반응하는 것으로 보인다.

저 ‘/‘ 때문에 ‘/test’ 에 대한 접근이 아니라 ‘//test’로 처리하기 때문에 나타나는 문제가 아닐까 추측해본다.


실행가능한 JAR(executable jar)을 생성하게 되면 다음과 같이 META-INF 폴더 밑에 MANIFEST.MF 파일이 생성된다.

├── Application.class
└── META-INF
    └── MANIFEST.MF

MANIFEST.MF 내용

  • Manifest specification
    Manifest-Version: 1.0
    Class-Path: .
    Main-Class: Application // 실행가능한jar의 엔트리포인트가 되는 클래스명
    

예제

  • java.util.jar.JarFile API를 이용해서 Jar 파일에 대한 정보를 읽어드릴 수 있다.
  • 다음과 같이 JDK6과 JDK7에서 가져올 수 있는 부분이 다른 것으로 보인다.

    ㅡ_-);; JDK6, JDK7, JDK8에서 다를 수 있는데 그걸 테스트하기가 좀… 귀찮…

JarFile jarFile = new JarFile(/** 식별하려고 하는 jar 파일*/);

private boolean hasMainClassManifest(JarFile jarFile) throws IOException {
    Double javaVersion = Double.parseDouble(System.getProperty("java.specification.version"));
    log.debug("Java version: {}", javaVersion);
    log.debug("Has Main-Class: {}", jarFile.getManifest().getEntries().containsKey("Main-Class"));
    log.debug("Has Main-Class: {}", jarFile.getManifest().getMainAttributes().getValue("Main-Class"));
    if(1.7 > javaVersion) {
        return jarFile.getManifest().getEntries().containsKey("Main-Class");
    } else if(1.7 == javaVersion) {
        return null != jarFile.getManifest().getMainAttributes().getValue("Main-Class");
    } else {
        //TODO JDK 8 에서는 어떻게 될까?
        log.debug("Not implements");
        return false;
    }
}

대략 위의 메서드를 통해서 선택한 jar가 실행가능한 Main-Class를 가지고 있는지 여부를 확인할 수 있다.

참고


최근…
이전과는 다르게 협업을 하고, 다른 이들의 코딩을 지도하고, 기술전파를 하고 교육을 하고…
하는 등의 일들을 동시다발적으로 진행하면서 내 부족함을 새삼 깨닫는다.

전과는 다른 역할을 수행하고 있기 때문이겠지.

    특히나 부족함을 느끼는 것이 ‘정확한 의사전달력’이다.

이야기를 나누고 리뷰를 하다보면 말의 언성이 높아지면서 스트레스를 담게 되는 몹쓸 습관이 있다.
이건 다른 사람들과 대화를 나누는 것에 대한 경험이 부족했기 때문이겠지.

즉흥적이게 이야기를 하는 편인 내게,
현재의 상황과 다음 상황을 고려하면서 이야기를 주고받는 것이 쉬운 일이 아니다.


올해 들어서,
다른 사람들과 함께 일하는데 필요한 능력들이 부족함이 여실히 드러났다.
이건… 어떻게 하면 일정수준까지 향상시킬 수 있을까?

1···29303132333435···97

+ Recent posts