반응형

일정 시간마다 특정 URL의 화면을 캡처하여 저장해달라는 요청이 있었다.

 

자바로 스케줄러를 구성하고 URL에 해당하는 정보를 가지고와서 JEditorPane 통해 이미지 컨텐츠를 구성하고 파일을 생성하는 예제이다. JEditorPane는 자바2에서부터 존재했던 클래스로 다양한 컨텐츠를 편집하기 위한 텍스트 컴퍼넌트이다.

해당 컴포넌트는 EditorKit 구현을 사용하여 동작하여 상속을 받은 클래스를 사용했습니다.

 

 

JAVA로 웹페이지 화면 저장하기

테스트용 화면 JSP페이지

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<html>
<head>
    <link rel="stylesheet" type="text/css" href="/assets/css/test.css">
</head>
<body>
    <div>
        <p class="title">o 금일 할일</p>
        <p>- 프로젝트 검수</p>
        <p>- GIT 최신화</p>
    </div>
</body>
</html>

 

 

파일 생성처리

import java.awt.Container;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;

import javax.imageio.ImageIO;
import javax.swing.JEditorPane;
import javax.swing.SwingUtilities;
import javax.swing.text.Document;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;

public class App {
	public static void main(String[] args) {
		BufferedImage ire;
		// 저장할 서버 url
		String requestUrl = "http://localhost/test/view";

		// 저장될 위치 + 파일명
		String path = "E:/test/test.jpg";

		App app = new App();
		// 사이즈 크기 설정 및 생성
		ire = app.create(requestUrl, 200, 200);
		try {
			ImageIO.write(ire, "PNG", new File(path));
		} catch (IOException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		}
	}

	static class Kit extends HTMLEditorKit {
		private static final long serialVersionUID = 2048542251827518481L;

		public Document createDefaultDocument() {
			HTMLDocument doc = (HTMLDocument) super.createDefaultDocument();
			doc.setTokenThreshold(Integer.MAX_VALUE);
			doc.setAsynchronousLoadPriority(-1);
			return doc;
		}
	}

	public BufferedImage create(String src, int width, int height) {
		BufferedImage image = null;
		JEditorPane pane = new JEditorPane();
		Kit kit = new Kit();
		pane.setEditorKit(kit);
		pane.setEditable(false);
		pane.setMargin(new Insets(0, 20, 0, 20));

		try {
			pane.setPage(new URL(src));
			image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
			Graphics g = image.createGraphics();
			Container c = new Container();
			SwingUtilities.paintComponent(g, pane, c, 0, 0, width, height);
			g.dispose();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return image;
	}
}

 

 

동작결과

 

다만, 웹페이지의 100% 유동적인 이미지로 생성하는 방식은 찾지를 못했다. web page에서 특정 데이터에 사이즈를 담아두고 파싱부분에서 처리하면 가능할 수도 있겠지만, 응답을 해주지 않는 타사이트라면 해당 방식 적용도 어려울 것으로 보인다.

 

 

반응형
  1. 익명 2022.07.07 19:32

    비밀댓글입니다

    • 익명 2022.07.08 09:01

      비밀댓글입니다

반응형

자바에서 Stream 객체과 관련된 객체를 사용하다보면, finally부분에서 사용한 자원을 해제하는 로직을 입력하거나 예외처리가 발생한 부분에서 해제시켜야 했습니다.

아래 소스를 보겠습니다.

 

 

기존 자원 해제 방식

1. catch문에서 자원해제

public class TryWithResources {

	public void readTxtFile(String path) {
		BufferedReader bufferedReader = null;
		try {
			bufferedReader = new BufferedReader(new FileReader("e:\\text.txt"), 16 * 1024);
			String str;
			while ((str = bufferedReader.readLine()) != null) {
				System.out.println(str);
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			try {
				if(bufferedReader != null) bufferedReader.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		} catch (IOException e) {
			e.printStackTrace();
			try {
				if(bufferedReader != null) bufferedReader.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		} 
	}
}

catch문이 많아진다면... 해제해야할 자원이 많아진다면... 상상만 해도 끔찍합니다.

 

 

2. finally를 통한 마지막에서 해제

public class TryWithResources {

	public void readTxtFile(String path) {
		BufferedReader bufferedReader = null;
		try {
			bufferedReader = new BufferedReader(new FileReader("e:\\text.txt"), 16 * 1024);
			String str;
			while ((str = bufferedReader.readLine()) != null) {
				System.out.println(str);
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if(bufferedReader != null) bufferedReader.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}
	}
}

finally는 무조건 실행되므로 마지막에 전부 자원이 해제됩니다.

catch가 더 많았다면 finally로 처리하는 모습이 비교적 가독성은 더 좋아보일 것 같습니다.

 

Try With Resources

public class TryWithResources {

	public void readTxtFile(String path) {
		try (BufferedReader bufferedReader = new BufferedReader(
        						new FileReader("e:\\text.txt"), 16 * 1024);){
			String str;
			while ((str = bufferedReader.readLine()) != null) {
				System.out.println(str);
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} 
	}
}

try() 안에 자원 객체를 사용하면 try를 벗어날때 자동으로 자원을 해제해줍니다.

 

Java7부터 생긴 기능으로 자원을 실수로 해제하지 않는 실수를 막아줄 수 있기에 자원을 사용하고 예외처리를 해야할 때,  사용한다면 좋을 것 같습니다.

 

다만, 모든 자원을 무조건 해제해주는것은 아니고 AutoCloseable인터페이스로 구현된 객체들만 자원이 해제됩니다.

즉, 개발자가 작성한 코드도 자동으로 자원을 해제하고 싶다면 AutoCloseable인터페이스를 상속받아서 작성하셔야 합니다.

 

반응형
반응형

https://myhappyman.tistory.com/244

 

JAVA - replaceFirst 치환시 주의점(특수문자 문자열 치환)

$user_id 형태로 되어있는부분을 찾아서 해당하는 특정값 'admin' 형태로 치환하는 특정 로직을 구성했는데 replace가 replaceAll처럼 뒷부분까지 동작하는 현상을 발견하였습니다. 😥 replace 정규식으로

myhappyman.tistory.com

 

이전에도  replaceFirst를 사용하여 문서를 읽으면서 차례대로 치환처리를 해주는 서비스를 개발했었는데, 관련하여 최근에 또 이슈가 발생하였다.

 

java.lang.IllegalArgumentException: Illegal group reference 해당 오류가 발생하고 있었는데, 로그부분의 관련된 서비스 로직부분을 보니 이번에도 replace부분이였다.

 

프론트에서 입력한 파라미터값을 가져와서 html을 만드는 서비스였는데, 특정 위치에 치환을 위해 replaceFirst부분을 사용하였고, 입력한 파라미터값에 '$', '\' 값이 존재하면 해당 오류가 발생하고 있었다.

 

자바 정규식에 특수문자 몇 키워드는 특별한 동작을 하도록 설계가 되어있어서 해당 현상이 발생한다.

이외에도 '+', '*', '?', '^' 등의 키워드가 정규식내에서 특정 키워드로 동작을 하는데, 이런 동작을 하지 않도록 처리할 수 있다.

 

java.util.regex.Matcher.quoteReplacement 메소드를 사용하면 되는데, 기존에 파라미터 처리하던 부분을 아래와 같이 수정하여 해결하였다.

 

기존 문제 발생

reportForm = reportForm.replaceFirst("[$]user_name",  param.get("user_name"))


수정 후

reportForm = reportForm.replaceFirst(
				"[$]user_name",  
				java.util.regex.Matcher.quoteReplacement(param.get("user_name"))
             )

 

반응형
반응형

올바르지 않은 난수 취약점

자바에서 랜덤 난수를 발생시킬때 보통 Math.random()을 많이 사용하여 작성하였는데, 해당 메소드의 사용은 예상가능한 난수를 사용하는것으로 시스템 보안에 약점을 유발한다고 합니다...😥

 

해당 메소드는 시드값을 설정 할 수 없고 사용하는 알고리즘이 밝혀지면 취약해질 수 있기때문이라고 합니다.

 

결론은 해당 메소드 대신 차용할 메소드가 필요한데, 제시된 방법으로 java.util.Random

또는 java.security.SecureRandom 클래스 사용을 추천합니다.

 

이런 문제로 기존에 작성된 부분을 SecureRandom으로 변경하였습니다.

 

https://docs.oracle.com/javase/8/docs/api/java/security/SecureRandom.html

 

SecureRandom (Java Platform SE 8 )

This class provides a cryptographically strong random number generator (RNG). A cryptographically strong random number minimally complies with the statistical random number generator tests specified in FIPS 140-2, Security Requirements for Cryptographic Mo

docs.oracle.com

 

사용법👀

Math.random()과 SecureRandom 클래스의 각각 사용법 비교를 해보겠습니다.

 

간단한 int 데이터 변수 생성 방법

- Math.random()

int div = (int) Math.floor( Math.random() * 2 )

 

- SecureRandom

SecureRandom sr = new SecureRandom();
int div = sr.nextInt(2);

정수형 난수 데이터를 구할때 올림, 내림처리도 필요없고 타입이 지정된 난수를 호출하기에 캐스팅도 없습니다.

앞으로 랜덤값을 생성해주는 SecureRandom클래스 자주 사용해야겠습니다.

반응형
반응형

$user_id 형태로 되어있는부분을 찾아서 해당하는 특정값 'admin' 형태로 치환하는 특정 로직을 구성했는데

replace가 replaceAll처럼 뒷부분까지 동작하는 현상을 발견하였습니다. 😥

 

replace 정규식으로 동작

public class App {
	public static void main(String args[]) {
	    String res = "$user_address, $user_address_detail";
	    res = res.replace("$user_address", "서울특별시 구로구");
	    System.out.println(res);
	}	
}

이유는 $로 인한 정규식으로 동작하였고, 첫번째 찾은 문자열만 변경하고 싶었던 저는 다음으로 찾은 메소드는 replaceFirst를 사용하여 처리하였지만, 다음과 같은 현상을 발견하였습니다.

 

replaceFirst 문제소스

public class App {
	public static void main(String args[]) {
		String res = "my name is $name";
		res = res.replaceFirst("$name", "psw");
		System.out.println(res);
	}	
}

치환이 안됨...

replace메소드에서 치환이 잘되던 replaceFirst가 $ 달러 특수기호를 붙이자 동작이 안되었습니다.🤷‍♂️

해결방법으로 JAVA에서 []로 묶어주게되면 해당부분을 강제로 문자열로 인식하게 할 수 있어 해당방법을 통해 해결하였습니다.

 

변경소스

public class App {
	public static void main(String args[]) {
		String res = "my name is $name";
		res = res.replaceFirst("[$]name", "psw");
		System.out.println(res);
	}	
}

치환이 정상적으로 된다.

 

반응형
반응형

Zip파일 내부에 존재하는 파일리스트 확인하기

pom.xml

apache.commons 추가

<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-compress</artifactId>
	<version>1.8</version>
</dependency>

ZipArchiveInputStream, ZipArchiveEntry 사용을 위해 추가합니다.

 

 

apache.tika 추가

<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-core</artifactId>
    <version>1.14</version>
</dependency>

파일의 MIME Type을 체크하기 위해 Tika 추가

 

 

ZipUtils.java

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.tika.Tika;

public class ZipUtils {

	public static void zipChecker(String filePath) {
		InputStream in = null;
		ZipArchiveInputStream zais = null;
		ZipArchiveEntry entry = null;
		Tika tika = new Tika();
		try {
			File file = new File(filePath);
			if(file.exists()) {
				in = new FileInputStream(new File(filePath));
				zais = new ZipArchiveInputStream(in, "UTF-8", true);
				while((entry = zais.getNextZipEntry()) != null) {
					System.out.println(tika.detect(entry.getName()));
					System.out.print(entry.getTime() + "  ");
					System.out.print(entry.getName() + "  ");
					System.out.print(entry.getComment() + "  ");
					System.out.print(entry.getCompressedSize() + "  ");
					System.out.print(entry.getCrc() + "  ");
					System.out.print(entry.getPlatform() + "  ");
					System.out.print(entry.getUnparseableExtraFieldData());
					System.out.println();
				}
			}
			
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				if(in != null) in.close();
				if(zais != null) zais.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

 

Main.java

public class App {
	public static void main(String[] args) {
		ZipUtils.zipChecker("E:/zip/test.zip");
	}
}

 

 

결과

 

압축 파일 리스트

반응형
반응형

SSH 연결 후 Shell 명령 및 Sftp 전송하기

pom.xml

<!-- SSH -->
<dependency>
  <groupId>com.jcraft</groupId>
  <artifactId>jsch</artifactId>
  <version>0.1.55</version>
</dependency>

 

SSHUtil.java

import java.io.ByteArrayOutputStream;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;

public class SSHUtil {
	
	private Session session;
	private Channel channel;
	private ChannelExec channelExec;
	private ChannelSftp channelSftp;
	private long percent = 0;
	
	public SSHUtil() {}
	
	public SSHUtil(String name, String password, String host, int port) {
		this.connect(name, password, host, port);
	}
	
	public SSHUtil connect(String name, String password, String host, int port) {
		try {
			session = new JSch().getSession(name, host, port);
			session.setPassword(password);
			session.setConfig("StrictHostKeyChecking", "no");
			session.connect();
		} catch (JSchException e) {
			e.printStackTrace();
		}
		
		return this;
	}
	
	public String command(String command) {
		if(session == null) {
			return "ssh 연결이 되지 않았습니다.";
		}else if(command == null || command.trim().equals("")) {
			return "명령어를 입력해주세요.";
		}
		
		String responseString = "";
		try {
			ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
			
			channelExec = (ChannelExec) session.openChannel("exec");
			channelExec.setCommand(command);
			channelExec.setOutputStream(responseStream);
			channelExec.connect();
	        while (channelExec.isConnected()) {
	            Thread.sleep(100);
	        }
	        
	        responseString = new String(responseStream.toByteArray());
		} catch (JSchException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
	        this.disConnect();
	    }
		return responseString;
	}
	
	public String fileSend(String OutPutPath, String DestinyPath) {
		percent = 0; //초기화
		if(session == null) {
			return "ssh 연결이 되지 않았습니다.";
		}else if(OutPutPath == null || DestinyPath == null) {
			return "경로가 정상적으로 등록되지 않았습니다.";
		}
		
		try {
			channelSftp = (ChannelSftp) session.openChannel("sftp");
			channelSftp.connect();
			channelSftp.put(OutPutPath, DestinyPath, new SystemOutProgressMonitor());
		} catch (JSchException e) {
			e.printStackTrace();
		} catch (SftpException e) {
			e.printStackTrace();
		} finally {
			this.disConnect();
		}
		return "정상적으로 전송하였습니다.";
	}
	
	public void disConnect() {
		if (session != null) {
            session.disconnect();
        }
        if (channel != null) {
            channel.disconnect();
        }
        if(channelExec != null) {
        	channelExec.disconnect();
        }
        if(channelSftp != null) {
        	channelSftp.disconnect();
        }
	}
	
	public long getSendPercent() {
		if(percent >= 100) {
			percent = 0;
			return (long)100;
		}
		return percent;
	}
	
	class SystemOutProgressMonitor implements SftpProgressMonitor {
		private long fileSize = 0;
		private long sendFileSize = 0;
		
		@Override
		public void init(int op, String src, String dest, long max) {
			this.fileSize = max;
			System.out.println("Starting : " + op + "  " + src + " -> " + dest + " total : " + max);
		}

		@Override
		public boolean count(long count) {
			this.sendFileSize += count;
			long p = this.sendFileSize * 100 / this.fileSize;
			if(p > percent) {
				percent++;
			}
			return true;
		}

		@Override
		public void end() {
			System.out.println("Finished!");
		}
	}
}

connect : 입력 파라미터값에 따른 연결 처리

command : 파라미터값으로 명령어 처리 및 결과 받기

fileSend : 읽어들일 경로, 전송할 경로 옵션에 따라 전송처리

disConnect : SSH 연결 종료

getSendPercent : 파일전송되고 있는 퍼센트값 return

SystemOutProgressMonitor : 내부 클래스로 SftpProgressMonitor 인터페이스를 상속받고 init, count, end 메소드에 따라 동작한다. 파일 전송시 put메소드에서 3번째 파라미터값에서 new 생성자로 사용된다.

 

상황에 맞춰 사용하시면 됩니다.

 

 

 

반응형
반응형

Collection

웹 프로젝트를 수행하면서 DB에서 데이터를 가져오는 과정에서 List<Vo> 또는 List<Map> 형태 등으로 데이터를 가져오는 행위를 진행하기도하고, 데이터를 가공하거나 처리할 때 컬렉션을 자주 사용합니다. 컬렉션에 대해서 알아보겠습니다.

 

먼저 컬렉션 그룹에는 3가지 타입(List, Set, Map)이 존재합니다.

 

각각에 대한 특정은 아래와 같습니다.

1. List : 데이터의 순서가 보장되며 중복을 허용하는 집합

2. Set : 순서를 유지하지 않고 중복도 허용하지 않는 집합

3. Map : key와 value가 pair로 이루어진 데이터의 집합으로 key의 중복은 허용되지 않습니다.(value는 허용)

 

개발시 컬렉션의 특징을 정확하게 파악하여 용도에 맞게 적용해야 하므로 3가지의 차이점을 알고 있어야 합니다.

 

Collection Method

boolean add(Object o)
boolean addAll(Collection c)
객체를 추가합니다.
void clear() 모든 객체를 삭제합니다.
boolean contains(Object o) 객체가 포함되어 있는지 확인합니다.
boolean equals(Object o) 동일한 컬렉션인지 비교합니다.
int hashCode() 컬렉션의 hasCode를 반환합니다.
boolean isEmpty() 컬렉션이 비어있는지 확인합니다.(비었으면 true)
Iterator iterator() 컬렉션에서 Iterator를 얻어서 한개씩 처리합니다.
boolean remove(Object o) 지정한 객체를 삭제합니다.
boolean removeAll(Collection c) 지정한 Collection의 모든 객체를 삭제합니다.
boolean retainAll(Collection c) 지정된 컬렉션만 남기고 나머지는 삭제합니다.
int size() 컬렉션의 개수를 가져옵니다.
Object[] toArray() 컬렉션의 데이터를 배열로 복사합니다.
Object[] toArray(Object[] a) 컬렉션의 데이터를 지정한 타입의 배열로 복사합니다.

 

List

리스트는 순서가 유지되며 중복도 허용합니다.

 

List Method

void add(int i, Object o) i위치에 o를 추가합니다.
Object get(int i) i의 객체를 반환합니다.
int indexOf(Object o) o 객체의 위치를 첫번째 요소부터 찾아서 반환합니다.
int lastIndexof(Object o) o 객체의 위치를 마지막 요소부터 찾아서 반환합니다.
ListIterator listIterator() List의 객체를 하나씩 반환하여 처리합니다.
Object remove(int i) i의 객체를 삭제하고 삭제된 객체를 반환합니다.
Object set(int i, Object o) 지정된 위치에 o 객체를 저장합니다.
void sort(Comparator c) List를 정렬합니다.
List subList(int fi, int ei) fi부터 ei까지의 범위의 객체를 반환합니다.

 

Map

key, value를 통해 하나의 쌍으로 동작하는 컬렉션 클래스로 key는 중복이 불가능하고 value는 중복이 가능합니다.

key중복시 마지막으로 등록한 key의 데이터로 덮어쓰기 됩니다.

 

Map Method

void clear() Map의 모든 객체를 제거합니다.
boolean containsKey(Object key) key와 일치하는 객체가 있는지 확인합니다.
boolean containsValue(Object value) value와 일치하는 개체가 있는지 확인합니다.
Set entrySet() 모든 key-value쌍의 객체를 저장한 Set으로 반환합니다.
boolean equasl(Object o) 동일한 Map인지 비교합니다.
Object get(Object key) key의 value값을 반환합니다.
boolean isEmpty() Map이 비어있는지 확인합니다.
Set keySet() 모든 key를 반환합니다.
Object put(Object key, Object value) key, value의 쌍을 지정하여 저장합니다.
void putAll(Map m) m의 모든 key, value를 저장합니다.
Object remove(Object key) key객체와 일치하는 쌍을 삭제합니다.
int size() key, value 쌍의 개수를 반환합니다.
Collection values() 저장된 모든 value 객체를 반환합니다.

 

반응형