Thymeleaf 에서 스프링 환경변수 사용하기

Thymeleaf 에서 스프링 프로퍼티 값 사용하는 방법

  • @ 뒤에 빈(Bean) 이름을 사용하면 그 빈에 접근할 수 있다.

타임리프에서 스프링의 property 파일(application.property 혹은 application.yml 등)에 기술되어 있는 변수를 이용하려는 경우

${@environment.getProperty('property.key')}

프로파일 환경에 따라 표시를 하려면

<div th : if = $ { @environment .acceptsProfiles ( 'production' )}>
This is the production profile
</ div>
or
<div th : if = "$ {# arrays.contains (@ environment.getActiveProfiles () 'production')} " >
     This is the production profile
</ div>

시스템 환경변수를 이용하는 경우

$ { @systemProperties [ 'property.key' ]}


타임리프의 확장은 쉽다. 방언(다이얼렉트,Dialect)를 생성하고 템플릿엔진에 추가하면 된다.

Dialect

타임리프 다이얼렉트는 템플릿에서 사용할 수 있는 기능이다.

다음과 같이 동작하는 다이얼렉트를 작성해보자.

<p hello:sayto="Jake">Hi ya!</p>

모든 다이얼렉트 는 IDialect 를 구현해야한다. 이를 용이하게 할 수 있도록AbstractDialect 을 이용한다. IProcessorDialect 는 실제로 Dialect 가

HelloDialect
public class HelloDialect extends AbstractDialect implements IProcessorDialect {
    public static final String PREFIX = "hello";
    public static final int PRECEDENCE = 501;
 
    public TimezoneDateDialect() {
        super("hello-dialect");
    }
 
    @Override
    public String getPrefix() {
        return PREFIX;
    }
 
    @Override
    public int getDialectProcessorPrecedence() {
        return PRECEDENCE;
    }
 
    @Override
    public Set<IProcessor> getProcessors(String dialectPrefix) {
        Set<IProcessor> iProcessors = Sets.newHashSet()
        iProcessors.add(new SayToProcessor(dialectPrefix)); (1)
        return iProcessors;
    }
}
뭔가~ 다른 것들을 추가할 수 있을 것 같지 않은가??

그럼 이제 이 다이얼렉트가 호출하여 실체 로직처리를 수행할 프로세스를 작성해보자.

SayToProcessor
public class SayToProcessor extends AbstractStandardExpressionAttributeTagProcessor {
 
    public static final String ATTR_NAME = "sayTo";
    public static final int PRECEDENCE = 1501;
 
    public SayToProcessor(String dialectPrefix) {
        super(TemplateMode.HTML, dialectPrefix, ATTR_NAMEPRECEDENCEtrue);
    }
 
    @Override
    protected void doProcess(ITemplateContext contextIProcessableElementTag tagAttributeName attributeNameString attributeValueObject expressionResultIElementTagStructureHandler structureHandler) {
        //TODO 비즈니스로직처리!! 
        structureHandler.setBody("Hello, " + expressionResult, false);
    }
}

작성한 다이얼렉트 를 템플릿엔진에 추가하자.

@Bean
public SpringTemplateEngine templateEngine() {
    SpringTemplateEngine engine = new SpringTemplateEngine();
    engine.setTemplateResolver(templateResolver());
    engine.setMessageSource(messageSource);
    engine.addDialect(new HelloDialect());
    return engine;
}

을 구현하고 나면!! 끝이 난다. 실제로 화면을 불러와 보면

<p>HelloJake!</p>

으로 출력되는 것을 볼 수 있을 것이다.


스프링부트 1.3.0이 나오고 현재 개발중인 애플리케이션의 적용버전을 바로 업그레이드 했다.

스프링부트 1.2.6 에서 1.3.0 으로 변경사항은

  • 스프링 4.1 → 4.2.1

  • 스프링시큐리티 3 → 스프링시큐리티 4

    • thymeleaf-extra-springsecurity3 → thymeleaf-extra-springsecurity4

정도의 차이가 생기겠다.

현재 ViewTemplate로 타임리프Thymeleaf 를 사용중인데, 스프링시큐리티와 연동해서 사용자가 가진 권한에 따른 처리를 하기 위해서 thymeleaf-extra-springsecurity(<https://github.com/thymeleaf/thymeleaf-extras-springsecurity>) 를 추가적으로 사용한다.

3 버전에서 4 버전으로 변경되면서 권한을 가지고 있는지 체크하는 구문이 변경된 것으로 보인다. * 3 버전

<div sec:authorize="hasRole('ROLE_USER')"></div>
<div sec:authorize="hasAnyRole('ROLE_ADMIN', 'ROLE_MANAGER')></div>
  • 4 버전

<div sec:authorize="hasAuthority('ROLE_USER')"></div>
<div sec:authorize="hasAnyAuthority('ROLE_ADMIN', 'ROLE_MANAGER')></div>

Role이 Authority 로 변경된 듯 싶다. ㅡ_-)>

스프링시큐리티 설정에서 URL별 접근권한을 설정하는 부분에서 hasAuthority를 사용하고 있어서 혹시나 하고 써봤더니 적용된다. 스프링시큐리티 문서를 보니 hasRole 등 Role 을 접미사로 사용하는 메서드는 ROLE_이라고 하는 접두사를 사용한 권한을 체크하는 목적으로 사용하는 것으로 보인다.

흠…​ ㅡ_-)> ROLE_ 붙이기 싫어서 빼먹고 권한을 정의했는데, 4버전에서 먹통이 되는 현상을 발생시키다니…​

정리

스프링시큐리티 4 적용 후 hasRole이나 hasAnyRole이 적용되지 않는다면 권한에 ROLE_ 접미사를 붙였는지 확인해보자.

레퍼런스 문서를 보는게 제일 좋다.

참고


깔끔하게 보고 싶으면 아래 링크에서 보라.
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

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


타임리프Thymeleaf가 잠시 나를 열받게 만들었다. 
자바스크립트까지 손댈줄이야...
$(".navbar-brand>span").html("<img src='images/logo_innoquartz_white.png' alt='Inno Quartz' />");

이렇게 해놓은 것을 Thyleaf 에서 렌더링하면서

$(".navbar-brand>span").html("<img src="images/logo_innoquartz_white.png" alt="Inno Quartz" />");

이렇게 바꾸면서 브라우저에서

SyntaxError: missing ) after argument list

오류를 발생시킨다. 이를 해결하는 방법은,

$(".navbar-brand>span").html('<img src="images/logo_innoquartz.png" alt="Inno Quartz" />');

으로 변경하면 해결된다.



이와 관련한 내용을 SNS에 올렸다가 CDATA 처리하는 것이 낫지 않느냐는 의견에 생각이 확장된다. 하앍~

백엔드 개발자랍시고 백엔드이상은 넘어오질 않았는데, 이번 프로젝트에 Thymeleaf를 View Template Engine으로 채용하면서 이쪽도 신경을 써야할 상황이 되었다. 아직은 정리가 되지 않은 부분이 많은데, 올해 연말 남은 기간동안 정리를 쭈욱 하고 진행해둬야겠다.

http://msdn.microsoft.com/ko-kr/library/ms256076%28v=vs.110%29.aspx
http://wp.goodchois.kr/devtip/archives/297

Thymeleaf의 템플릿은 사실 HTML의 탈을 쓴 XML이다. html로 파일확장자를 사용하기 때문에 브라우저에서 별무리없이 렌더링을 해서 보여주는 거다.
thymeleaf 파일 내에 자바스크립트도 결국은 XML의 일부로 여기기 때문에 자바스크립트에 대해서도 변형을 진행하는 것으로 보인다.

<![CDATA[An in-depth look at creating applications with XML, using <, >,]]>

위의 형식과 같이 자바스크립트 관련한 항목을

 <script type="text/javascript">
    //<![CDATA[
    window.jQuery || document.write("<script src='assets/js/jquery-2.0.3.min.js'}'>"+"<"+"/script>");
    //]]>
</script>

와 같은 형태로 작성을 해야한다.

이보다 나은 방법은, 추후 어느정도 개발이 완료된 시점에 script 관련 항목을 별도의 스크립트 파일로 추출하여 모아놓고,
배포시에 스크립트 관련항목을 컴파일하여 압축하고 하나로 합쳐주는 작업을 진행하는 것이 더 적절해보인다.

12

+ Recent posts