728x90

프로젝트에서 PDF를 통합하는 솔루션을 고민하다가 간단한 기능만(병합,분리) 필요하니 그냥 오픈소스를 활용하는것도 나쁘지 않을것 같아서 작성하였다.

iText vs PDFBox 둘중 고민하다가 그냥 PDFBox 를 사용하기로 했다. google 검색하니 소스가 널려 있어서 잘 되는걸로 가져다가 조금 수정하였다.

 

pom.xml 에서 library 추가.

<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>3.0.0-RC1</version>
</dependency>

java 코드는 아래 와같다.

path는 맥북 기준이니 windows 사용자는 알아서 수정하면 될것 같다.

public static void main(String[] args) {

        //pdf 분리
        // 테스트 시 path는  맥북 기준...windows 경로는 알아서...
        separate(new File("/Users/blake/Documents/pdf/pns_1_5.pdf"),2);
        //pdf 합치기
        File file1=new File("/Users/blake/Documents/pdf/merge_1.pdf");
        File file2=new File("/Users/blake/Documents/pdf/merge_2.pdf");
        List<File> files= List.of(file1,file2);
        merge(files,"/Users/blake/Documents/pdf/new_merge.pdf");
    }



    public static boolean separate(File file, int page){
        try {
            PDDocument document = Loader.loadPDF(file);
            int pages=  document.getNumberOfPages();
            if(pages>page){
                separate(file,1,page);
                return separate(file,page+1,pages);
            }
        } catch (IOException e) {
            return false;
        }
        return false;
    }
    public static boolean separate(File file,int startPage,int endPage){
        return separate(file,endPage-startPage+1,startPage,endPage);
    }
    public static boolean separate(File file,int page,int startPage,int endPage){
        try {
            //pdf 파일 로드
            PDDocument document = Loader.loadPDF(file);
            // 분리함수사용하기 위한 객체 생성.
            Splitter splitter = new Splitter();
            splitter.setStartPage(startPage);
            splitter.setSplitAtPage(page);
            splitter.setEndPage(endPage);
            // 분리후 파일 리스트
            List<PDDocument> Pages = splitter.split(document);

            Iterator<PDDocument> iterator = Pages.listIterator();
            //분리된 파일 저장
            int i = 1;
            while (iterator.hasNext()) {
                PDDocument pd = iterator.next();
                String folderPath= file.getParent();
                String fileName=file.getName().substring(0,file.getName().lastIndexOf("."));
                int start=startPage+(i-1)*page;
                int end=i*page>endPage?i*page:endPage;
                pd.save(folderPath+File.separator+fileName+(start+"~"+end)+".pdf");
            }
            System.out.println("PDF 분리완료!");
            document.close();
            return true;
        }catch (Exception e){
            return false;
        }
    }

    public static boolean merge(List<File> files,String path){
        PDFMergerUtility merger = new PDFMergerUtility();
        //파일 merge 설정
        merger.setDestinationFileName(path);
        files.forEach(f->{
            try {
                merger.addSource(f);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        });
        try {
            //병합시작...
            merger.mergeDocuments(null);
            System.out.println("PDF merge 완료!");
        } catch (IOException e) {
            return  false;
        }
        return true;
    }

스크린샷 2023-03-07 오후 3.17.29.png
0.05MB

 

728x90

현재 springboot은 기본으로 .yml 을 우선시한다. 하지만 가끔씩 아날로그식? .properties 구석 과 xml 구성으로 값을 가져와 써야 할때가 종종 있다. 그럼 어떻게 사용할까?

poml.xm 추가

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-configuration-processor</artifactId>
     <optional>true</optional>
 </dependency>

application.properties

author.name=samsung
author.age=20

설정클래스 상단에 @PropertySource("classpath:your.properties")을 사용하면 yml 구성을 로딩한것과 같은 효과다.

@Component
@PropertySource(value = {"classpath:static/config/authorSetting.properties"},
        ignoreResourceNotFound = false, encoding = "UTF-8", name = "authorSetting.properties")
public class AuthorTest {
 
    @Value("${author.name}")
    private String name;
    @Value("${author.age}")
    private int age;
}
@PropertySource 의 속성 요약 1.value:설정파일 경로. 2.ignoreResourceNotFound:지정파일 존재하지 않을시 에러! 기본옵션 false 이다.true 설정해야 파일 없으면 오류. 실제 개발시 ignoreResourceNotFound 를 false로 설정한다. 3.encoding:파일 읽어드릴시 UTF-8 인코딩.
 
@Value 사용이 좀 많을때 등장한것이 바로 @ConfigurationProperties 옵션이다.
@Component
@ConfigurationProperties(prefix = "author")
@PropertySource(value = {"classpath:static/config/authorSetting.properties"},
        ignoreResourceNotFound = false, encoding = "UTF-8", name = "authorSetting.properties")
public class AuthorTest {
 
    private String name;
    private int age;
 
}
@RestController
@EnableConfigurationProperties
public class DemoController {
 
    @Autowired
    AuthorTest authorTest;
 
    @RequestMapping("/")
    public String index(){
        return "author's name is " + authorTest.getName() + ",ahtuor's age is " + authorTest.getAge();
    }
}

마지막으로!

@ConfigurationProperties 사용하기위 위해서는 꼭 @EnableConfigurationProperties 를 써서 활성화 한다.

 

끝!

 

728x90

 

Spring 에서 @Transactional 사용하기 전에 @EnableTransactionManagement 사용했었다.

그래서 인지 몰라서 그시절 spring 쌉고수들이 springboot 으로 갈아타면서 @EnableTransactionManagement 을 꼭 써야한다는 주장을 하는걸 봐서 오늘 한판하고 왔다.

그래서 오늘 포스팅은 springboot 기준으로 @EnableTransactionManagement 을 써야 될지한번 보자!

springboot 의 메인인 @SpringBootApplication 을 타고 들어가보자.

다시 @EnableAutoConfiguration 도 타고 들어가보자.

 

이런것도 있네

여기에서 확인할수 있는것처럼 트랜잭션관리는 AOP 기반으로 JDK Dynamic Proxy와 CGLIB 2가지 방식을 정의하였다.

 

결론 : Spring 아닌 Springboot 에서는 자동으로 트랜잭션구성을 해주기때문에 @EnableTransactionManagement 를 달아주는 뻘짓은 이제 좀 자제하자!

 

반박시 니말 다 맞음 ( 너무 싼티났나 ? ^^ )

끝!

728x90

Springboot 으로 email 전송기능을 구현 하는 방법에는 다양하게 존재한다고 한다.

오늘은 내가 알고 있는 방법으로 글을 써볼까한다.

쓰레드 환경에서도 많은 사람한테 보낼수 있는 기능이라고 가져다가 자기 환경에 맞게 조금 수정하면 잘 써질듯하다.

controller 에서 responseEntity 하는 부분은 대충 써놔서 수정이 조금 필요한듯 싶다.

그럼 코드 go.go.go

pom.xml 에 추가 

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

application.yml 파일 작성

server:
  port: 8080

spring:
  mail:
    host: smtp.gmail.com
    username: your_google_account@gmail.com
    password: mxmiujqrxzgfabcd
    default-encoding: UTF-8
    properties:
      mail:
        smtp:
          auth: false
          starttls:
            enable: true
            required: true

 

멀티스레드 풀 관련 config 작성

@EnableAsync
@Configuration
public class ExecutorConfig extends AsyncConfigurerSupport {

    /**
     * 스레드풀 사이즈 설정
     **/
    private int corePoolSize = 4;
    /**
     * 스레드풀 초대 사이즈 설정
     **/
    private int maxPoolSize = 16;
    /**
     * 스레드풀 큐 사이즈
     **/
    private int queueCapacity = 10;
    /**
     * 스레드 이름 접두사
     */
    private String threadNamePrefix = "EmailAsyncExecutor-";

    @Bean("EmailAsync")
    public ThreadPoolTaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setThreadNamePrefix(threadNamePrefix);


        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        executor.setWaitForTasksToCompleteOnShutdown(true);

        executor.setAwaitTerminationSeconds(60);

        executor.initialize();

        return executor;
    }

}

Email 객체 생성

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Builder
public class EmailModel implements Serializable {

	/**
	 * 초기화
	 */
	private static final long serialVersionUID = 1404185636399251685L;

	/**
	 * 수신자
	 */
	private String email;
	/**
	 * 메일 제목
	 */
	private String subject;
	/**
	 * 메일 내용
	 */
	private String content;
	/**
	 * 첨부파일 위치
	 */
	private String attachFilePath;
	/**
	 * 메일 resource 위치
	 */
	private String resourcePath;
	/**
	 * 메일 resource 이름
	 */
	private String resourceName;

}

Email Service 작성

package com.example.demo.service.impl;

import com.example.demo.model.EmailModel;
import com.example.demo.service.MailService;
import lombok.RequiredArgsConstructor;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;


@Service
@RequiredArgsConstructor
public class MailServiceImpl implements MailService {


    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(MailServiceImpl.class);
    private final JavaMailSender mailSender;
    @Value("${spring.mail.username}")
    private String from;

    @Async("EmailAsync")
    @Override
    public Boolean sendSimpleMail(EmailModel emailModel) {

        try {
            SimpleMailMessage message = new SimpleMailMessage();
            message.setFrom(from);// 보내는 사람, 설정 하지 않으면 기본 값 .properties 값이 셋팅된다.
            message.setTo(emailModel.getEmail().split(";"));//수신자  1명 혹은 여러명일때 ";" 로 분리
            message.setSubject(emailModel.getSubject());// 제목
            message.setText(emailModel.getContent());// 본문

            mailSender.send(message);

            logger.info("단순 문자전자 메일전송 성공!");
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("단순 문자전자 메일전송 실패!");
            return false;
        }
        return true;
    }

    @Async("EmailAsync")
    @Override
    public Boolean sendHtmlMail(EmailModel emailModel) {

        String to = emailModel.getEmail();
        String subject = emailModel.getSubject();
        String content = emailModel.getContent();
        logger.info("HTML Template 메일전송 시작:{},{},{}", to, subject, content);
        //MimeMessage,MIME 프로토콜 사용
        MimeMessage message = mailSender.createMimeMessage();

        MimeMessageHelper helper;
        //MimeMessageHelper 더욱 많은 내용들을 설정 할수 있다.
        try {
            helper = new MimeMessageHelper(message, true);
            helper.setFrom(from);
            helper.setTo(to.split(";"));
            helper.setSubject(subject);
            helper.setText(content, true);// ture 일시 html 지원
            mailSender.send(message);
            logger.info("HTML Template 메일전송 성공!");
        } catch (MessagingException e) {
            logger.error("HTML Template 메일전송 실패!", e);
            return false;
        }
        return true;
    }

    @Async("EmailAsync")
    @Override
    public Boolean sendAttachmentMail(EmailModel emailModel) {


        String to = emailModel.getEmail();
        String subject = emailModel.getSubject();
        String content = emailModel.getContent();
        String filePath = emailModel.getAttachFilePath();
        logger.info("첨부파일 메일전송 시작:{},{},{},{}", to, subject, content, filePath);
        MimeMessage message = mailSender.createMimeMessage();

        MimeMessageHelper helper;
        try {
            helper = new MimeMessageHelper(message, true); //true 파일 여러개라는 뜻
            helper.setFrom(from);
            helper.setTo(to.split(";"));
            helper.setSubject(subject);
            helper.setText(content, true);
            FileSystemResource file = new FileSystemResource(new File(filePath));
            String fileName = file.getFilename();
            helper.addAttachment(fileName, file);//添加附件,可多次调用该方法添加多个附件
            mailSender.send(message);
            logger.info("첨부파일 메일전송 성공!");
        } catch (MessagingException e) {
            logger.error("첨부파일 메일전송 실패!", e);
            return false;
        }
        return true;
    }

    @Async("EmailAsync")
    @Override
    public Boolean sendInlineResourceMail(EmailModel emailModel) {

        String to = emailModel.getEmail();
        String subject = emailModel.getSubject();
        String content = emailModel.getContent();
        String resourcePath = emailModel.getResourcePath();
        String resourceName = emailModel.getResourceName();
        logger.info("첨부파일 메일전송 시작:{},{},{},{},{}", to, subject, content, resourcePath, resourceName);
        MimeMessage message = mailSender.createMimeMessage();

        MimeMessageHelper helper;
        try {
            helper = new MimeMessageHelper(message, true);
            helper.setFrom(from);
            helper.setTo(to.split(";"));
            helper.setSubject(subject);
            helper.setText(content, true);
            // 절대경로를 읽는다
            FileSystemResource res = new FileSystemResource(new File(resourcePath));
            helper.addInline(resourceName, res);//파일여러개 일때  여러번 사용가능
            mailSender.send(message);
            logger.info("첨부파일 메일전송 성공!");
        } catch (MessagingException e) {
            logger.error("첨부파일 메일전송 실패!", e);
            return false;
        }
        return true;
    }

    @Async("EmailAsync")
    @Override
    public Boolean sendHtmlImageMail(EmailModel emailModel) {

        String to = emailModel.getEmail();
        String subject = emailModel.getSubject();
        String content = emailModel.getContent();
        String resourcePath = emailModel.getResourcePath();
        logger.info("첨부파일 메일전송 시작:{},{},{},{}", to, subject, content, resourcePath);
        MimeMessage message = mailSender.createMimeMessage();

        MimeMessageHelper helper;
        try {
            helper = new MimeMessageHelper(message, true);
            helper.setFrom(from);
            helper.setTo(to.split(";"));
            helper.setSubject(subject);
            helper.setText(content, true);

            helper.setText("<html><head></head><body><h1>hello world!</h1>" + "<img src=\"cid:aaa\"/></body></html>", true);
            FileSystemResource img = new FileSystemResource(new File(resourcePath));

            helper.addInline("aaa", img);

            mailSender.send(message);
            logger.info("첨부파일 메일전송 성공!");
            return true;

        } catch (MessagingException e) {
            logger.error("첨부파일 메일전송 실패!", e);
            return false;

        }
    }
}

서비스를 호출할 Controller 작성

@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/mail")
public class MailController {


    private final MailService mailService;


    @PostMapping("/test")
    public ResponseEntity sendSimpleMail(String toEmail) {
        mailService.sendSimpleMail(EmailModel
                .builder()
                .email(toEmail)
                .subject("test")
                .content("일단 아무거나 테스트")
                .build());
        return ResponseEntity.ok().build();
    }


    @PostMapping("/simple")
    public ResponseEntity sendSimpleMail(@RequestBody EmailModel emailModel) {

        mailService.sendSimpleMail(emailModel);
        return ResponseEntity.ok().build();
    }


    @PostMapping("/html")
    public ResponseEntity sendHtmlMail(@RequestBody EmailModel emailModel) {

        mailService.sendHtmlMail(emailModel);
        return ResponseEntity.ok().build();
    }


    @PostMapping("/attach")
    public ResponseEntity sendAttachmentMail(@RequestBody EmailModel emailModel) {

        mailService.sendAttachmentMail(emailModel);
        return ResponseEntity.ok().build();
    }


    @PostMapping("/resource")
    public ResponseEntity sendInlineResourceMail(@RequestBody EmailModel emailModel) {

        mailService.sendInlineResourceMail(emailModel);
        return ResponseEntity.ok().build();
    }


    @PostMapping("/image")
    public ResponseEntity sendHtmlImageMail(@RequestBody EmailModel emailModel) {

        mailService.sendHtmlImageMail(emailModel);
        return ResponseEntity.ok().build();
    }


}

POSTMAN 으로 이메일 테스트 해볼게 

네이버에서 메일에서 확인

트러블슈팅

아래와 같이 에러가 나온다.

구글 보안 문제로 구글패스워드 아닌 앱 키를 발급받아서 적용해야 된다. 다른 편번으로 임시로 테스트하는 방법도 있지만 보안문제로 그냥 이대로 쓰는거 편하다.

setp1:

setp2:

mxmiujqrxzgfabcd

이런 유형의 문자열을 복사해서 yml 파일 설정 항목중 password 해당부분에 넣으면 된다.

 

게으른 당신을 위한 소스코드 https://github.com/luxury515/luxury515-springboot-email-thread

 

이상끝!

+ Recent posts