hls.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<link href="https://vjs.zencdn.net/7.11.4/video-js.css" rel="stylesheet" />
<script src="https://vjs.zencdn.net/7.11.4/video.min.js"></script>
</head>
<body>
<video id="video" class="video-js" controls="controls" width="400" autoplay="autoplay"></video>


<script th:inline="javascript">
/*<![CDATA[*/
var videoUrl = /*[[ ${videoUrl} ]]*/;
/*]]*/
videojs.Vhs.xhr.beforeRequest = function (options) {
    options.headers = {
        Authorization: 'Bearer ' + "userToken"
    };
    return options;
};
var $vplay = videojs("video");
$vplay.src({
	src : videoUrl,
	type: "application/x-mpegurl"
});
</script>
  
</body>
</html>

 

 

java

	@GetMapping("/hls-make/{fileName}")
	@ResponseBody
	public void videoHlsMake(@PathVariable String fileName, Model model) throws IOException {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		
    	final String FFMPEG_PATH = ffmpegProperties.getPath();
    	final String FFMPEG = ffmpegProperties.getFfmpeg();
    	final String FFPROBE = ffmpegProperties.getFfprobe();
    	final String FILEPATH = UPLOAD_DIR + "/" + fileName;
    	final String ONLY_FILENAME = fileName.substring(0, fileName.lastIndexOf("."));
    	final String TS_PATH = UPLOAD_DIR + "/" + ONLY_FILENAME;
    	File tsPath = new File(TS_PATH);
    	if(!tsPath.exists()) {
    		tsPath.mkdir();
    	}
    	
		FFmpeg ffmpeg = new FFmpeg(FFMPEG_PATH + "/" + FFMPEG);
		FFprobe ffprobe = new FFprobe(FFMPEG_PATH + "/" + FFPROBE);
		
        FFmpegProbeResult probeResult = ffprobe.probe(FILEPATH);

        
        log.debug("========== VideoFileUtils.getMediaInfo() ==========");
        log.debug("filename : {}", probeResult.getFormat().filename);
        log.debug("format_name : {}", probeResult.getFormat().format_name);
        log.debug("format_long_name : {}", probeResult.getFormat().format_long_name);
        log.debug("tags : {}", probeResult.getFormat().tags.toString());
        log.debug("duration : {} second", probeResult.getFormat().duration);
        log.debug("size : {} byte", probeResult.getFormat().size);
        log.debug("width : {} px", probeResult.getStreams().get(0).width);
        log.debug("height : {} px", probeResult.getStreams().get(0).height);
        log.debug("===================================================");
		
        // TS 파일 생성
        FFmpegBuilder builder = new FFmpegBuilder()
        			//.overrideOutputFiles(true) // 오버라이드 여부
        			.setInput(FILEPATH) // 동영상파일
        			.addOutput(TS_PATH + "/" + ONLY_FILENAME + ".m3u8") // 썸네일 경로
        			.addExtraArgs("-profile:v", "baseline") // 
        			.addExtraArgs("-level", "3.0") //
        			.addExtraArgs("-start_number", "0") //
        			.addExtraArgs("-hls_time", "10") //
        			.addExtraArgs("-hls_list_size", "0") //
        			.addExtraArgs("-f", "hls") //
        			.done();
        FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
        executor.createJob(builder).run();
        
        
        // 이미지 파일 생성
        FFmpegBuilder builderThumbNail = new FFmpegBuilder()
    			.overrideOutputFiles(true) // 오버라이드 여부
    			.setInput(FILEPATH) // 동영상파일
    			.addExtraArgs("-ss", "00:00:03") // 썸네일 추출 싲가점
    			.addOutput(UPLOAD_DIR + "/" + ONLY_FILENAME + ".png") // 썸네일 경로
    			.setFrames(1) // 프레임 수
    			.done();
        FFmpegExecutor executorThumbNail = new FFmpegExecutor(ffmpeg, ffprobe);
        executorThumbNail.createJob(builderThumbNail).run();
        
		model.addAttribute("result", "OK");
	}
	
	
	@GetMapping("/hls")
	public String videoHls(Model model) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		model.addAttribute("videoUrl", "/video/hls/video/video.m3u8");
		return TEMPLATE_DIR + "hls";
	}
	
	@GetMapping("/hls/{fileName}/{fileName}.m3u8")
	public ResponseEntity<Resource> videoHlsM3U8(@PathVariable String fileName) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		String fileFullPath = UPLOAD_DIR + fileName + "/" + fileName + ".m3u8";
		Resource resource = new FileSystemResource(fileFullPath); 
		HttpHeaders headers = new HttpHeaders();
		headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName + ".m3u8");
		headers.setContentType(MediaType.parseMediaType("application/vnd.apple.mpegurl"));
		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
	}
	
	@GetMapping("/hls/{fileName}/{tsName}.ts")
	public ResponseEntity<Resource> videoHlsTs(@PathVariable String fileName, @PathVariable String tsName) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		String fileFullPath = UPLOAD_DIR + fileName + "/" + tsName + ".ts";
		Resource resource = new FileSystemResource(fileFullPath); 
		HttpHeaders headers = new HttpHeaders();
		headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + tsName + ".ts");
		headers.setContentType(MediaType.parseMediaType(MediaType.APPLICATION_OCTET_STREAM_VALUE));
		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
	}

 

pom.xml

		<!-- ffmpeg 동영상 인코더 연결 라이브러리 -->
		<dependency>
		  <groupId>net.bramp.ffmpeg</groupId>
		  <artifactId>ffmpeg</artifactId>
		  <version>0.6.2</version>
		</dependency>

 

 

https://www.ffmpeg.org/download.html

 

Download FFmpeg

If you find FFmpeg useful, you are welcome to contribute by donating. More downloading options Git Repositories Since FFmpeg is developed with Git, multiple repositories from developers and groups of developers are available. Release Verification All FFmpe

www.ffmpeg.org

 

이클립스 UTF-8설정을 안하고 프로젝트를 임포트하는 경우

html,css,jsp 파일등이 euc-kr 로 설정된느 경우가 있다.

 

파일 하나씩 열어서 UTF-8로 변경하거나 프로젝트 삭제 후 다시 임포트 하면 되지만

한번에 모든 파일을 UTF-8로 업데이트 하는 방법이 있다.

 

메뉴 > Window > Preferences > General > Content Types 메뉴에서

Text 를 클릭하고 하단의 Default encoding 항목에 UTF-8을 입력하고

Update 버튼을 클릭하면 모든 파일이 UTF-8로 업데이트 된다.

 

업데이트하는데 시간이 좀 걸리니 조금 있다 파일을 다시 열어보면 깨져있던 한글이 정상적으로 보인다.

 

 

 

LocalDateTime format, pattern으로 변환하여 표현하기

 

패턴, 포맷별 출력 형식이다.

 

LocalDateTime date = LocalDateTime.now(

date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))
// 2021-09-02 14:56:20.669

date.format(DateTimeFormatter.BASIC_ISO_DATE)
// 20210902
date.format(DateTimeFormatter.ISO_DATE)
// 2021-09-02
date.format(DateTimeFormatter.ISO_DATE_TIME)
// 2021-09-02T14:56:20.669
date.format(DateTimeFormatter.ISO_INSTANT)
// ERROR
date.format(DateTimeFormatter.ISO_LOCAL_DATE)
// 2021-09-02
date.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
// 2021-09-02T14:56:20.669
date.format(DateTimeFormatter.ISO_OFFSET_DATE)
// ERROR
date.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
// ERROR
date.format(DateTimeFormatter.ISO_ORDINAL_DATE)
// 2021-245
date.format(DateTimeFormatter.ISO_TIME)
// 14:56:20.669
date.format(DateTimeFormatter.ISO_WEEK_DATE)
// 2021-W35-4
date.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
// ERROR
date.format(DateTimeFormatter.RFC_1123_DATE_TIME)
// ERROR

date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL))
// 2021년 9월 2일 목요일
date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG))
// 2021년 9월 2일 (목)
date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
// 2021. 9. 2
date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
// 21. 9. 2

date.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.FULL))
// ERROR
date.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG))
// 오후 2시 56분 20초
date.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM))
// 오후 2:56:20
date.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT))
// 오후 2:56

date.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL))
// ERROR
date.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG))
// 2021년 9월 2일 (목) 오후 2시 56분 20초
date.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))
// 2021. 9. 2 오후 2:56:20
date.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT))
// 21. 9. 2 오후 2:56

 

querydsl 에서 서브쿼리로 exists 를 사용하는 예제이다.

쿼리 성능상 in 보다는 exists 를 사용하는게 유리하다.

게시글 댓글에 특정 단어가 있는 게시물을 검색하는 쿼리이다.

 

쿼리문

SELECT * 
FROM Board A
WHERE EXISTS (SELECT 1 FROM Comment WHERE no = A.no AND content LIKE '%내용%')
;

 

위 쿼리를 querydsl 로 표현하면 아래와 같다.

 

querydsl

List<Board> list = 
	query.selectFrom(board)
		.where(JPAExpressions.selectFrom(comment).where(comment.board.eq(board)
        	.and(comment.content.contains("내용"))).exists())
		.fetch();

 

'Java > JPA' 카테고리의 다른 글

JPA QueryDSL 에서 MAX 엔티티 가져오기  (0) 2019.10.07

스프링부트에서 @Valid로 유효성 검사 시

Integer 등 숫자형에 문자열이 입력된 경우 typeMismatch 에러가 발생하며 아래와 같은 메세지가 결과물로 온다.

Failed to convert property value of type java.lang.String to required type java.lang.Integer for property age; nested exception is java.lang.NumberFormatException: For input string: "abcd"

 

이 문구를 바로 사용자에게 보여줄 수 없으므로 해당 에러인 경우 에러 메세지를 커스텀화 해야 한다.

 

dto

@Getter
@Setter
@ToString
public class PersonForm {

	@NotBlank(message="이름을 입력해 주세요.")
	@Size(min=2, max=30, message="글자수가 안맞아요.")
	private String name;
	
	@NotNull(message="나이를 입력해 주세요.")
	@Min(value=18, message="나이는 최소한 18세이상으로 입력하세요.")
	private Integer age;
}

 

controller

	@PostMapping("/form")
	public String validation_valid(@Valid PersonForm personForm, BindingResult bindingResult) {
		if (bindingResult.hasErrors()) {
			return TEMPLATE_DIR.concat("form");
		}
		return "redirect:/" + TEMPLATE_DIR.concat("result");
	}

 

 

html

<form id="frm" action="#" method="post" th:action="@{/validation/form}" th:object="${personForm}">
	name : <input th:field="*{name}"><br>
	<div class="error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"> Name Error</div>
	age : <input th:field="*{age}"><br>
	<div th:if="${#fields.hasErrors('age')}" th:errors="*{age}"> Age Error</div>
	<button type="submit">Submit</button>
</form>

 

이런 구성인 경우 age에 문자를 입력 시 위와 같은 에러 메세지가 표시된다.

 

스프링부트인 경우

resources 폴더아래에

messages.properties 파일을 생성하고 아래와 같이 입력해 주면 된다.

typeMismatch.java.lang.Integer=숫자로 입력해 주세요.

thymeleaf 에서 외부 사이트 특정 부위 insert(include) 하기

 

html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<h1>head</h1>

<!-- 주소 :: 태그명.클래스명 -->
<div th:insert="https://www.thymeleaf.org :: section.description" >...</div>

<h1>foot</h1>


</body>
</html>

 

config 파일에 아래와 같이 설정해 줘야 한다.

configuration 파일이 없다면 임의로 하나 만들면 된다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.templateresolver.UrlTemplateResolver;

@Configuration
public class SpringTemplateConfig {
    // UrlTemplateResolver를 빈으로 등록
    @Bean
    public UrlTemplateResolver urlTemplateResolver() {
        return new UrlTemplateResolver();
    }
}

스프링부트 자동 빌드 - 자바, thymeleaf 자동 리로드, autoreload, livereload

 

 

pom.xml에 스프링부트 데트툴 디팬던시 추가

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
  <scope>runtime</scope>
  <optional>true</optional>
</dependency>

 

 

application.yml 에 라이브리로드 추가

spring:
  devtools:
    livereload:
      enabled: true
    restart:
      enabled: true

또는

spring.devtools.livereload.enabled=true

spring.devtools.restart.enabled=true

 

livereload는 뷰단 파일 thymeleaf 파일등 변경되었을때 다시 로드 

restart는 자바파일 변경 시 재 빌드.

 

이클립스 메뉴 Project > Build Automatically 체크

 

간단한 프로젝트에서 동영상 서비스를 해야 하는 경우가 생겼다.

프로젝트는 스프링 부트 설정이였고 접속자는 많지 않을것으로 예상.

보통은 스트리밍 서버를 사용하지만 그정도 규모의 프로젝트가 아닌 경우에는

그냥 웹서버에 파일을 올리거나

아니면 자바에서 파일을 내려주는 형식으로 스트리밍 서비스를 하는 경우도 있다.

물론 사용자가 많지 않아서 부하가 적다는 가정하에서 말이다.

 

아래는 자바에서 동영상 서비스를 하는 여러가지를 정리했다.

 

1. 웹경로에 파일을 올려서 바로 바로 스트리밍 하는 방법

2. 자바에서 파일을 읽어 내려주는 방법

3. 자바에서 파일을 읽어 내려주는 방법(Resource 객체로 내림)

4. 자바에서 파일의 일부분만 읽어서 부분적으로 파일을 내려주는 방법

5. HLS를 이용하여 스트리밍을 지원하는 방법

 

video.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<video controls="controls" th:src="${videoUrl}" width="400" autoplay="autoplay"></video>
</body>
</html>

 

 

package com.test2.web.video;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
@RequestMapping("/video")
public class VideoController {

	private final String TEMPLATE_DIR = "video/";
	private final String UPLOAD_DIR = "D:\\upload\\";
	
	// 1. 웹경로에 의한 스트리밍
    // 스프링프로젝트의 /resources/static/video 폴터에 동영상을 넣어 서비스
    @GetMapping("")
	public String videoIndex(Model model) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		model.addAttribute("videoUrl", "/video/video.mp4");
		return TEMPLATE_DIR + "video";
	}

	
    // 2. 다운로드 방식으로 서비스
    // byte array 로 내려주는 경우 스트리밍이 아닌 다운로드 방식이므로 전체 파일이 다운로드 된 이후 플레이 된다.
    // 크롬인 경우 플에이 이후 시간 설정이 되지 않는다.
	@GetMapping("/download")
	public String videoDownload(Model model) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		model.addAttribute("videoUrl", "/video/download/video.mp4");
		return TEMPLATE_DIR + "video";
	}
    
	@GetMapping("/download/{fileName}")
	public void vidoeDownloadFileName(@PathVariable String fileName, HttpServletRequest req, HttpServletResponse res) throws IOException {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());


		String fileFullPath = UPLOAD_DIR + fileName;
		File downFile = new File(fileFullPath);  //파일 객체 생성
		if(downFile.isFile()){  // 파일이 존재하면
			int fSize = (int)downFile.length();
			res.setBufferSize(fSize);
			res.setContentType("application/octet-stream");
			res.setHeader("Content-Disposition", "attachment; filename="+fileName+";");
			res.setContentLength(fSize);  // 헤더정보 입력
			FileInputStream in  = new FileInputStream(downFile);
			ServletOutputStream out = res.getOutputStream();
			try
			{
				byte[] buf=new byte[8192];  // 8Kbyte 로 쪼개서 보낸다.
				int bytesread = 0, bytesBuffered = 0;
				while( (bytesread = in.read( buf )) > -1 ) {
					out.write( buf, 0, bytesread );
					bytesBuffered += bytesread;
					if (bytesBuffered > 1024 * 1024) { //아웃풋스트림이 1MB 가 넘어가면 flush 해준다.
						bytesBuffered = 0;
						out.flush();
					}
				}
			}
			finally {
				if (out != null) {
					out.flush();
					out.close();
				}
				if (in != null) {
					in.close();
				}
				//에러가 나더라도 아웃풋 flush와 close를 실행한다.
			}
		}
		
	}
	
    // 3. 동영상 파일을 Resource 객체로 내려주는 경우
    // 파일이 전체 내려오지 않아도 동영상 플레이가 시작된다. 단 중간에 플레이 구간을 선택 시 다시 파일을 받기 시작한다.
	@GetMapping("/resource")
	public String videoResource(Model model) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		model.addAttribute("videoUrl", "/video/resource/video.mp4");
		return TEMPLATE_DIR + "video";
	}
	
	@GetMapping("/resource/{fileName}")
	public ResponseEntity<Resource> vidoeResourceFileName(@PathVariable String fileName) throws FileNotFoundException {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		String fileFullPath = UPLOAD_DIR + fileName;
		Resource resource = new FileSystemResource(fileFullPath); 
		HttpHeaders headers = new HttpHeaders();
		headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName + "");
		headers.setContentType(MediaType.parseMediaType("video/mp4"));
		return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK);
	}
	
	
	// 4. 범위설정을 하여 파일의 일부분만 내려주는 형식
    // 동영상 시작시 설정된 파일 크기를 내리고 video 태그에서 호출하는 단위 크기만큼만 계속 내려주는 형식	
    // 플레이 구간을 선택 시 해당하는 구간부터 파일을 내려받는다.
	@GetMapping("/region")
	public String videoRegion(Model model) {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		model.addAttribute("videoUrl", "/video/region/video.mp4");
		return TEMPLATE_DIR + "video";
	}
	
	@GetMapping("/region/{fileName}")
	public ResponseEntity<ResourceRegion> vidoeRegionFileName(@PathVariable String fileName, @RequestHeader HttpHeaders headers) throws IOException {
		log.debug("************** class = {}, function = {}", this.getClass().getName(), new Object(){}.getClass().getEnclosingMethod().getName());
		String fileFullPath = UPLOAD_DIR + fileName;
		Resource resource = new FileSystemResource(fileFullPath);
		
		final long chunkSize = 1024 * 1024 * 1;
		long contentLength = resource.contentLength();
		ResourceRegion region;
		
		try {
			HttpRange httpRange = headers.getRange().stream().findFirst().get();
			long start = httpRange.getRangeStart(contentLength);
			long end = httpRange.getRangeEnd(contentLength);
			long rangeLength = Long.min(chunkSize, end - start + 1);
			region = new ResourceRegion(resource, start, rangeLength);

			log.debug("**********  httpRange :: rangeLength ==> {}", rangeLength);
		} catch (Exception e) {
			long rangeLength = Long.min(chunkSize, contentLength);
			region = new ResourceRegion(resource, 0, rangeLength);
			log.debug("**********  httpRange error :: rangeLength ==> {}", rangeLength);
		}
        return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES))
        		.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
        		.header("Accept-Ranges", "bytes")
        		.eTag(fileName) // IE 부분 호출을 위해서 설정
        		.body(region);
	}

	
}

 

 

 

/**
 * 파일다운로드
 * @param request 
 * @return
 * @throws Exception
 */
@RequestMapping(value = "download.do")
public void downloadFileController(HttpServletRequest req, HttpServletResponse res) throws Exception {
	String mode = req.getParameter("mode");
	String file = req.getParameter("file");
	file = file.replaceAll("\\.\\.\\/", "");  // 부정접근 방지
	String name = req.getParameter("name");
	name = URLEncoder.encode(name, "UTF-8");  //한글깨짐 방지
	String folder = "temp";
	if("stamp".equals(mode)){
		folder = "stamp";
	}else if("course".equals(mode)){
		folder = "course";
	}else if("test".equals(mode)){
		folder = "test";
	}  // 폴더 접근 유효성 체크
	String fileFullPath = ROOT_UPLOAD_DIR+File.separator+folder+File.separator+file;  // 서버에 맞는 업로드 기본 폴더
	File downFile = new File(fileFullPath);  //파일 객체 생성
	if(downFile.isFile()){  // 파일이 존재하면
		int fSize = (int)downFile.length();
		res.setBufferSize(fSize);
		res.setContentType("application/octet-stream");
		res.setHeader("Content-Disposition", "attachment; filename="+name+";");
		res.setContentLength(fSize);  // 헤더정보 입력
		FileInputStream in  = new FileInputStream(downFile);
		ServletOutputStream out = res.getOutputStream();
		try
		{
			byte[] buf=new byte[8192];  // 8Kbyte 로 쪼개서 보낸다.
			int bytesread = 0, bytesBuffered = 0;
			while( (bytesread = in.read( buf )) > -1 ) {
				out.write( buf, 0, bytesread );
				bytesBuffered += bytesread;
				if (bytesBuffered > 1024 * 1024) { //아웃풋스트림이 1MB 가 넘어가면 flush 해준다.
					bytesBuffered = 0;
					out.flush();
				}
			}
		}
		finally {
			if (out != null) {
				out.flush();
				out.close();
			}
			if (in != null) {
				in.close();
			}
			//에러가 나더라도 아웃풋 flush와 close를 실행한다.
		}
	}
}

자바에서 파일을 클라이언트로 내려줄때 heap메모리나 out of memory가 날때가 있다.
서버 메모리가 작아서 문제일 수도 있지만
자바 소스에서 사용한 메모리를 회수하지 못해서 생기는 문제일때도 많다.

기본 반향은 파일을 일정 크기로 쪼개서 내보내고
버퍼에 담긴것은 날려줘야 한다.


Integer 는 객체.. 값을 비교할려면  == 가 아닌 equals 를 써야 한다.

package test2.object;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@SpringBootTest
public class IntegerEquals {

	@Test
	public void test() {
		Integer i1 = 127;
		Integer i2 = 127;
		log.debug(" i1 == i2 = {}, i1.equals(i2) = {}", i1==i2, i1.equals(i2));
		// i1 == i2 = true, i1.equals(i2) = true
        
		Integer i3 = 128;
		Integer i4 = 128;
		log.debug(" i3 == i4 = {}, i3.equals(i4) = {}", i3==i4, i3.equals(i4));
		// i3 == i4 = false, i3.equals(i4) = true
        
		Integer i5 = 128;
		int i6 = 128;
		log.debug(" i5 == i6 = {}, i5.equals(i6) = {}", i5==i6, i5.equals(i6));
		// i5 == i6 = true, i5.equals(i6) = true
	}
	
}

 

테스트에서 보는 봐와 같이 

127까지는 Integer 도 == 로 비교 시 제대로 비교된다. 하지만 128부터는 equals 를 해야 한다.

int 와 Integer 를 비교하는 경우에는 == 비교 시 묵시적 형변환으로 제대로 비교되는걸 알수 있다.

+ Recent posts