tomcat의 shutdown.sh 를 실행해서 서비스는 불가능 한 상태가 되지만 tomcat 프로세스는 살아 있는 경우가 있다.

tomcat의 서블릿이 

 

@WebListener

public class SchedulerManager implements ServletContextListener {

    @Override

    public void contextDestroyed(ServletContextEvent sce) {

        for (ThreadPoolTaskScheduler sch : SchedulingFongifurer.threadPoolTaskSchedulerList) { // 스케줄러 설정 class에서 스제출러 설정 목록을 static 으로 저장해 둔 목록

             sch.shutdown();

        }

    }

}

 

public String sampleServerResource() {
    String serverInfo = "";

    // storage
    String path = "/data";
    File file = new File(path);
    if (file.exists()) {
        serverInfo += String.format("Storage Info : path %s, total %s, free %s, usable %s <br>", 
                file.getAbsoluteFile(), addUnit(file.getTotalSpace()), addUnit(file.getFreeSpace()), addUnit(file.getUsableSpace()));
    }

    // CPU
    OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
    serverInfo += String.format("CPU Info : name %s, arch %s, version %s, avail %s, load average %s <br>", 
            osBean.getName(), osBean.getArch(), osBean.getVersion(), osBean.getAvailableProcessors(), osBean.getSystemLoadAverage());

    // Memory
    MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
    MemoryUsage heap = memoryMXBean.getHeapMemoryUsage();
    serverInfo += String.format("Heap Memory Info : init %s, used %s, commited %s, max %s <br>", 
            addUnit(heap.getInit()), addUnit(heap.getUsed()), addUnit(heap.getCommitted()), addUnit(heap.getMax()));
    MemoryUsage noneHeap = memoryMXBean.getNonHeapMemoryUsage();
    serverInfo += String.format("Heap None Memory Info : init %s, used %s, commited %s, max %s <br>", 
            addUnit(noneHeap.getInit()), addUnit(noneHeap.getUsed()), addUnit(noneHeap.getCommitted()), addUnit(noneHeap.getMax()));

    return serverInfo;
}

private String addUnit(long number) {
    String unitStr = "";
    if(number < 1024) return number + " byte";
    number /= 1024;
    if(number < 1024) return number + " Kb";
    number /= 1024;
    if(number < 1024) return number + " Mb";
    number /= 1024;
    if(number < 1024) return number + " Gb";
    number /= 1024;
    return number + " Tb";
}

 

시스템 자체의 CPU 사용량과 메모리 사용량을 보기 위해서는

oracle jdk 가 필요하다.

자바에서 POI를 이용해서 엑셀을 내려주면 되지만..

굳이 CSV를 만들어서 내려달라는 요청이 있는 경우도 있다.

 

각 데이터를 콤마로 연결된 텍스트 문서로 만들어서 내려줘도 되지만

opencsv 라는 편한 라이브러리가 있으니 그걸 사용하면 된다.

 

build.gradle

implementation 'com.opencsv:opencsv:5.5'

데이터 형식을 List<String[]> 스트링 배열의 리스트로 담아서 라이브러리에 던지면 한번에

CSV파일을 만들어 다운로드 할 수 있다.

 

서비스 Class

package com.study.ljj.sample.service;

import com.study.ljj.member.MemberDTO;
import com.study.ljj.sample.repository.SampleMapper;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class SampleService {

    private final SampleMapper sampleMapper;

    public SampleService(SampleMapper sampleMapper) {
        this.sampleMapper = sampleMapper;
    }

    public List<String[]> listMemberString() {
        List<MemberDTO> list = sampleMapper.listMember();
        List<String[]> listStrings = new ArrayList<>();
        listStrings.add(new String[]{"아이디", "이름", "비밀번호", "등록일"});
        for (MemberDTO member: list) {
            String[] rowData = new String[4];
            rowData[0] = member.getUserid();
            rowData[1] = member.getName();
            rowData[2] = member.getPassword();
            rowData[3] = String.valueOf(member.getRegdate());
            listStrings.add(rowData);
        }
        return listStrings;
    }

}

DB 에서 필요한 정보를 검색해와 List<String[]> 배열 형태로 변경하여 리턴한다.

 

Controller Class

package com.study.ljj.sample.web;

import com.opencsv.CSVWriter;
import com.study.ljj.sample.service.SampleService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Controller
public class CsvDownController {

    private final SampleService sampleService;

    public CsvDownController(SampleService sampleService) {
        this.sampleService = sampleService;
    }

    @GetMapping("/sample/csv/down")
    public void csvDown(HttpServletResponse response) throws IOException {
        response.setContentType("text/csv; charset=UTF-8"); // Set the character encoding
        String fileName = URLEncoder.encode("회원정보.csv", "UTF-8");
        response.setHeader("Content-Disposition", 
        	"attachment; filename=\"" + fileName + "\"");

        OutputStreamWriter writer = new OutputStreamWriter(response.getOutputStream(), 
        	StandardCharsets.UTF_8);
        writer.write("\uFEFF");
        CSVWriter csvWriter = new CSVWriter(writer);

        csvWriter.writeAll(sampleService.listMemberString());

        csvWriter.close();
        writer.close();
    }
}

Header 정보에 CSV파일임을 명시하고, UTF-8임을 명시한다.

파일명도 한글이 깨질수 있으므로 URL인코더로 UTF-8로 변경한다.

CSV파일로 내릴 Stream도 UTF-8로 설정하고

UTF-8 중에서 UTF-8-BOM임을 표시하기 위해 

writer.write("\uFEFF"); 을 추가한다. 해당 문자가 있으면 BOM형식임을 인식한다.

이후 서비스에서 받은 List<String[]> 정보를 opencsv 라이브러리에 보내면 CSV다운로드는 완료된다.

 

참고로

writer.write("\uFEFF"); 를 추가하지 않은 경우 UTF-8 파일로 제대로 내려와서 TEXT편집기로 읽는 경우 잘 나오지만

엑셀에서 읽는 경우 한글,중국어, 일본어, 아랍어등 UTF-8언어는 모두 깨진다.

위와 같이 모든 UTF-8 문자가 깨져서 보인다.

물론 엑셀에서 언어를 UTF-8로 설정하고 CSV를 임포하는 형식으로 하면 제대로 열린다.

깨지는것은 자동으로 열때 문자셋 정보가 완벽하지 않아서 깨지는 것이다.

 

writer.write("\uFEFF");

를 추가한 경우는

 

위와 같이 해당 언어가 UTF-8-BOM임을 명시하면

자동연결되어 엑셀로 열어도 문자가 깨지지 않는다.

1. 설정

그래이들

implementation 'org.springframework.boot:spring-boot-starter-cache'

config 설정

@Configuration
@EnableCaching
public class CacheConfig {
}

 

2. 사용

캐시 할 내용에 @Cacheable

캐시 삭제시 @CacheEvict

캐시 업데이트 시 @Cacheput

 

import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class CacheService {

    private final CacheManager cacheManager;

    public CacheService(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    @Cacheable(value = "random", key = "#id")
    public String cacheRandom( String id) {
        Random random = new Random();
        return id + "_" + random.nextInt(100);
    }

    @CacheEvict(value = "random", key = "#id")
    public void cacheRandomEvict(String id) {

    }

    public void cacheAll() {
        Collection<String> cacheNames = cacheManager.getCacheNames();
        for (String cacheName : cacheNames) {
            System.out.println("cacheName = " + cacheName);
        }
        Cache random = cacheManager.getCache("random");
        System.out.println("random = " + random);

        for (String cacheName : cacheNames) {
            Cache cache = cacheManager.getCache(cacheName);

            if (cache != null) {
                ConcurrentHashMap nativeCache = (ConcurrentHashMap) cache.getNativeCache();
                System.out.println("nativeCache = " + nativeCache);
                System.out.println("nativeCache type = " + nativeCache.getClass().getName() );
                ConcurrentHashMap cacheMap = (ConcurrentHashMap) nativeCache;
                cacheMap.forEach((strKey, strValue)->{
                    System.out.println( strKey +" : "+ strValue );
                });
            }
        }

    }
}

CacheManger (구현체 ConcurrentMapCacheManager )를 통해서 캐시 이름 목록을 가져오고

다시 이름으로 Cache 를 가져오고

가져온 캐시를 ConcurrentHashMap으로 변환 후 순회한다.

1) 포트 사용 에러

 

오류: 에이전트에 예외사항이 발생했습니다. : java.rmi.server.ExportException: Port already in use: 35199; nested exception is: 
java.net.BindException: Address already in use: JVM_Bind

 

실행 중지를 하지 않고 다시 실행하는 경우 위와 같은 에러가 종종 발생한다.

 

사용하려는 포트가 이미 사용중이라는 말로 사용중인 포트의 프로세스아이디를 찾아서 해당 프로세스를 죽여줘야 한다.

 

윈도우키+r -> cmd -> netstat 명령어로 사용중인 포트의 프로세스아이디를 찾아 킬하면 됩니다.

 

8080 사용 프로세스 검색 및 프로세스 종료시키기

 

cmd > netstat -ano | findstr 8080

실행을 하면 8080포트를 사용하는 프로세스 목록이 나옵니다.

가장 오른쪽의 프로세스 아이디를 킬해주면 됩니다.

cmd > taskkill /F /PID 8964

 

리소스 모니터에서 해당 프로세스 종료하기

작업관리자의 리소스모니터에서 해당 프로세스를 종료할 수 도 있습니다.

SpringBoot + Thymeleaf 로 작업 시 

 

html 변경이 바로 적용되지 않는 문제가 있다.

html 부분이나 스크립트 부분이나 고치면서 계속 확인 해야 하는 작업이 필요한 경우 

재실행하는거는 여간 귀찮은 일이 아니다.

 

applicatoin.properties에 아래와 같이 설정을 하면 바로 변경된 html을 확인 할 수 있다.

 

spring.thymeleaf.cache=false
spring.thymeleaf.prefix=file:src/main/resources/templates/

 

인텔리제이 Community 버전도 잘 된다.

 

 

 

Thymeleaf 적용한 프로젝트에서 title 부분을 전체 똑같이 적용해 달라는 요청이 있었다.

 

Thymeleaf Layout 이 적용된 상태에서

 

layout.html 

의 <title> 부분이 없는 상태였고

각 페이지에서 <title> 값을 설정한 상태였다.

layout.html 에 <title>LAYOUT</title> 을 추가해도

각 페이지의 <title> 이 layout.html 의 title을 대체하게 된다.

 

layout:title-pattern 속성을 이용하면 전체 페이지의 title을 패턴화 할수 있다.

lauout.html 에 아래 태그를 추가하면

<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">MY SITE</title>

$LAYOUT_TITLE - 레이아웃의 타이틀 (구버전 : $DECORATOR_TITLE 는 deprecated 됨)

$CONTENT_TITLE - 각 페이지의 타이틀

 

실제 각 페이지에서는

<title>MY SITE - 각페이지 타이틀</title> 과 같이 된다.

 

해당 요청 사항은

<title layout:title-pattern="$LAYOUT_TITLE">MY SITE</title>

로 해결했다.

 

참고 : title-pattern - Thymeleaf Layout Dialect (ultraq.github.io)

 

스프링부트 로컬에서 여러개 띄울때 세션 끊어지는 것 방지

 

application.properties에

server.session.cookie.name=특정단어

를 추가하면 된다.

외부 서버에서 파일을 ByteArrayInputStream으로 받아서 서버에 물리적으로 저장하는 방법

 

 

void org.apache.commons.io.FileUtils.copyInputStreamToFile(InputStream source, File destination) throws IOException

 

아파치 커먼 IOd에 있는 FileUtils 클래스를 이용하여 저장하면 된다.

 

FileUtils.copyInputStreamToFile(인풋스트림, 파일객체);

스프링에서 파라미터 유효성 검사를 위한 커스터 마이징

날짜형식의 String을 유효성 체크 하기 위한 커스터 마이징

날짜형식 : 2021-09-10

 

1. 의존성 추가

 

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

 

2. 사용자 정의 어노테이션을 위한 인터페이스 생성

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Documented
@Constraint(validatedBy = DateValidator.class)
@Target( {ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DateValid {
    String message() default "날짜형식이 옳바르지 않습니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

 

3. 유효성 검사를 실행할 클래스 생성

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class DateValidator implements ConstraintValidator<DateValid, String>{

    @Override
    public void initialize(DateValid constraintAnnotation) {

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null ||  value.length() == 0) return true;
        try {
            LocalDate.from(LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        } catch (DateTimeParseException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

 

4. DTO에 사용자 정의 유효성 검사 어노테이션 추가

 

@DateValid(message = "날짜형식이 옳바르지 않습니다.")
private String date;

 

 

+ Recent posts