반응형

스프링부트에서 소켓통신을 통한 채팅프로그램 만들기 2번째입니다.

 

이전까지의 소스는 아래 URL에서 참고해주세요.

https://myhappyman.tistory.com/100

 

SpringBoot - 스프링부트에서 채팅프로그램(소켓통신) 만들기-1

이번엔 소켓통신을 통하여 채팅프로그램을 스프링부트에서 만들어보겠습니다. 간단하게 프로젝트를 생성부터 채팅방 생성 및 채팅하는 과정까지 만들어보겠습니다. 소켓통신을 사용한 채팅프로그램 만들기 스프링..

myhappyman.tistory.com

 

 

 

1장에서는 단순하게 String 메시지 자체를 보냈는데, 이번엔 JSON형태로 메시지를 보내고 서버에서도 JSON형태의 메시지를 파싱하여 구분처리를 해보도록 하겠습니다.

추가적으로 내가보낸 메시지와 상대방을 구분해보겠습니다.

채팅구분처리


Server단

simple json 라이브러리 추가

pom.xml

<!-- json simple  -->
<dependency>
  <groupId>com.googlecode.json-simple</groupId>
  <artifactId>json-simple</artifactId>
  <version>1.1.1</version>
</dependency>
<!-- json simple  -->

pom.xml에 json파싱을 위해 json-simple 라이브러리를 추가합니다.

 

 

jsonToObjectParser 함수 추가

private static JSONObject JsonToObjectParser(String jsonStr) {
	JSONParser parser = new JSONParser();
	JSONObject obj = null;
	try {
		obj = (JSONObject) parser.parse(jsonStr);
	} catch (ParseException e) {
		e.printStackTrace();
	}
	return obj;
}

이전에 생성하였던 SocketHandler.java에 JSON파일이 들어오면 파싱해주는 함수를 추가하였습니다.

json형태의 문자열을 파라미터로 받아서 SimpleJson의 파서를 활용하여 JSONObject로 파싱처리를 해주는 함수입니다.

 

 

핸들러 로직 추가

SocketHandler.java

package com.psw.chating.handler;

import java.util.HashMap;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
public class SocketHandler extends TextWebSocketHandler {
	
	HashMap<String, WebSocketSession> sessionMap = new HashMap<>(); //웹소켓 세션을 담아둘 맵
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
		String msg = message.getPayload();
		JSONObject obj = jsonToObjectParser(msg);
		for(String key : sessionMap.keySet()) {
			WebSocketSession wss = sessionMap.get(key);
			try {
				wss.sendMessage(new TextMessage(obj.toJSONString()));
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
		super.afterConnectionEstablished(session);
		sessionMap.put(session.getId(), session);
		JSONObject obj = new JSONObject();
		obj.put("type", "getId");
		obj.put("sessionId", session.getId());
		session.sendMessage(new TextMessage(obj.toJSONString()));
	}
	
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		//소켓 종료
		sessionMap.remove(session.getId());
		super.afterConnectionClosed(session, status);
	}
	
	private static JSONObject jsonToObjectParser(String jsonStr) {
		JSONParser parser = new JSONParser();
		JSONObject obj = null;
		try {
			obj = (JSONObject) parser.parse(jsonStr);
		} catch (ParseException e) {
			e.printStackTrace();
		}
		return obj;
	}
}

1. 소켓이 연결되면 동작하는 afterConnectionEstablished() 메소드부분에 로직이 추가되었습니다.

생성된 세션을 저장하면 발신메시지의 타입은 getId라고 명시 후 생성된 세션ID값을 클라이언트단으로 발송합니다.

클라이언트단에서는 type값을 통해 메시지와 초기 설정값을 구분할 예정입니다.

 

2. 메시지 전송시 JSON파싱을 위해 message.getPayload()를 통해 받은 문자열을 만든 함수 jsonToObjectParser에 넣어서 JSONObject값으로 받아서 강제 문자열 형태로 보내주는부분이 추가되었습니다.

 

 

Client 단

이제 발송하는 chat.jsp를 수정하겠습니다.

 

클라이언트단 뷰페이지 추가 및 json형태로 요청 및 파싱하기

chat.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<meta charset="UTF-8">
	<title>Chating</title>
	<style>
		*{
			margin:0;
			padding:0;
		}
		.container{
			width: 500px;
			margin: 0 auto;
			padding: 25px
		}
		.container h1{
			text-align: left;
			padding: 5px 5px 5px 15px;
			color: #FFBB00;
			border-left: 3px solid #FFBB00;
			margin-bottom: 20px;
		}
		.chating{
			background-color: #000;
			width: 500px;
			height: 500px;
			overflow: auto;
		}
		.chating .me{
			color: #F6F6F6;
			text-align: right;
		}
		.chating .others{
			color: #FFE400;
			text-align: left;
		}
		input{
			width: 330px;
			height: 25px;
		}
		#yourMsg{
			display: none;
		}
	</style>
</head>

<script type="text/javascript">
	var ws;

	function wsOpen(){
		ws = new WebSocket("ws://" + location.host + "/chating");
		wsEvt();
	}
		
	function wsEvt() {
		ws.onopen = function(data){
			//소켓이 열리면 동작
		}
		
		ws.onmessage = function(data) {
			//메시지를 받으면 동작
			var msg = data.data;
			if(msg != null && msg.trim() != ''){
				var d = JSON.parse(msg);
				if(d.type == "getId"){
					var si = d.sessionId != null ? d.sessionId : "";
					if(si != ''){
						$("#sessionId").val(si); 
					}
				}else if(d.type == "message"){
					if(d.sessionId == $("#sessionId").val()){
						$("#chating").append("<p class='me'>나 :" + d.msg + "</p>");	
					}else{
						$("#chating").append("<p class='others'>" + d.userName + " :" + d.msg + "</p>");
					}
						
				}else{
					console.warn("unknown type!")
				}
			}
		}

		document.addEventListener("keypress", function(e){
			if(e.keyCode == 13){ //enter press
				send();
			}
		});
	}

	function chatName(){
		var userName = $("#userName").val();
		if(userName == null || userName.trim() == ""){
			alert("사용자 이름을 입력해주세요.");
			$("#userName").focus();
		}else{
			wsOpen();
			$("#yourName").hide();
			$("#yourMsg").show();
		}
	}

	function send() {
		var option ={
			type: "message",
			sessionId : $("#sessionId").val(),
			userName : $("#userName").val(),
			msg : $("#chatting").val()
		}
		ws.send(JSON.stringify(option))
		$('#chatting').val("");
	}
</script>
<body>
	<div id="container" class="container">
		<h1>채팅</h1>
		<input type="hidden" id="sessionId" value="">
		
		<div id="chating" class="chating">
		</div>
		
		<div id="yourName">
			<table class="inputTable">
				<tr>
					<th>사용자명</th>
					<th><input type="text" name="userName" id="userName"></th>
					<th><button onclick="chatName()" id="startBtn">이름 등록</button></th>
				</tr>
			</table>
		</div>
		<div id="yourMsg">
			<table class="inputTable">
				<tr>
					<th>메시지</th>
					<th><input id="chatting" placeholder="보내실 메시지를 입력하세요."></th>
					<th><button onclick="send()" id="sendBtn">보내기</button></th>
				</tr>
			</table>
		</div>
	</div>
</body>
</html>

 

 

아래는 chat.jsp의 로직 설명입니다.


css부분은 따로 설정하진 않겠습니다.

1. html 소스부분은 현재의 세션값을 저장해놓기 위해 sessionId가 ID인 input태그를 추가하였습니다.

2. send() 함수의 발송하기전에 단순 String데이터가 아닌 obj값으로 값을 세팅하고 JSON형태로 발신처리로 변경되었습니다.

메시지를 보낼땐 type값을 message로 구분하여 발송합니다.

 

3. Socket.onmessage의 메시지 전달시 받는 로직이 변경되었습니다.

서버에서도 데이터를 JSON형태로 전달해주기 때문에 받은 데이터를 JSON.parse메소드를 활용하여 파싱을 합니다.

파싱한 객체의 type값을 확인하여 getId값이면 초기 설정된 값이므로 채팅창에 값을 입력하는게 아니라 추가한 태그 sessionId에 값을 세팅해줍니다.

id값은 소켓이 종료되기 전까지 자기자신을 구분할 수 있는 session값이 될 예정입니다.

 

4. type이 message인 경우엔 채팅이 발생한 경우입니다.

상대방과 자신을 구분하기 위해 여기서 sessionId값을 사용합니다.

최초 이름을 입력하고 연결되었을때, 발급받은 sessionId값을 비교하여 같다면 자기 자신이 발신한 메시지이므로 오른쪽으로 정렬하는 클래스를 처리하고 메시지를 출력합니다.

비교하여 같지 않다면 타인이 발신한 메시지이므로 왼쪽으로 정렬하는 클래스를 처리하고 메시지를 출력합니다.

 

 

다음장에서는 방구분 처리를 해보겠습니다.

 

 

반응형
반응형

이번엔 소켓통신을 통하여 채팅프로그램을 스프링부트에서 만들어보겠습니다.

 

프로젝트를 생성해서 단순한 채팅방과 추가적으로 방생성에 따른 채팅 구분 등의 과정까지 만들어보겠습니다.

 

 

소켓통신을 사용한 채팅프로그램 만들기

스프링부트 프로젝트 생성하기

먼저 프로젝트부터 만들어야겠죠?

Spring Starter Project로 생성합니다.

 

값을 채워넣었습니다.

프로젝트명, group, artifact, package명 등 위 내용으로 채워넣었습니다.

생성하고자 하는 프로젝트로 만드세요~

 

WebSocket 사용

라이브러리는 WebSocket을 사용할거니까 검색 후 추가해주고, 완료해주겠습니다.

 

스프링부트 프로젝트 생성!

 

부트 프로젝트 설정하기(pom.xml)

view페이지는 jsp를 사용할 예정입니다.

사용을 위해 pom.xml 설정을 하겠습니다. 아래의 dependency도 추가해주세요.

 

pom.xml

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

<!-- View JSP -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>
<!-- View JSP -->

 

 

다음은 properties의 설정값을 추가하겠습니다.

톰캣 포트를 80으로 변경하고, jsp를 바라볼 수 있도록 설정하겠습니다.

또한, 재시작없이 jsp가 적용되는 설정까지 하도록 하겠습니다.

 

application.properties

 

resources/application.properties

#Tomcat Server Setting
server.port=80

#JSP, HTML ModelAndView Path Setting
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

#JSP to Modify Not Restart Server
server.servlet.jsp.init-parameters.development=true

 

 

뷰 설정

디렉토리 생성 및 chat.jsp파일 생성

뷰 페이지 구조

뷰페이지를 생성합니다.

이제 컨트롤러를 구성하여 넘어가는 것을 보고 바로 소켓통신 설정을 진행해보겠습니다.

 

 

컨트롤러 추가

MainController 추가

controller패키지 추가 및 MainContoller.java파일을 생성합니다.

package com.psw.chating.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MainController {
	
	@RequestMapping("/chat")
	public ModelAndView chat() {
		ModelAndView mv = new ModelAndView();
		mv.setViewName("chat");
		return mv;
	}
}

chat파일을 넘겨주는 view컨트롤러를 생성 후 서버 구동하여 정상적으로 jsp페이지에 접근이 되는지 확인합니다.

 

127.0.0.1/chat 접근

localhost/chat으로 접근하니 정상적으로 chat.jsp페이지에 접근하는 모습을 볼 수 있습니다.

이제 본격적으로 WebSocket처리를 연결하고 통신 예제를 진행해보겠습니다.

 

WebSocket 설정

웹소켓 구현체와 구현체등록

웹소켓 구현체와 등록해주는 config파일을 생성해보겠습니다. 위 그림처럼 패키지와 자바파일을 생성합니다.

 

SocketHandler.java

@Component
public class SocketHandler extends TextWebSocketHandler {
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
	}
	
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
	}
	
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		//소켓 종료
	}
}

구현체에 등록할 SocketHandler입니다.

afterConnectionEstablished 메소드는 웹소켓 연결이 되면 동작합니다.

acafterConnectionClosed 메소드는 반대로 웹소켓이 종료되면 동작합니다.

handleTextMessage 메소드는 메시지를 수신하면 실행됩니다. 상속받은 TextWebSocketHandler는 handleTextMessage를 실행시키며, 메시지 타입에따라 handleBinaryMessage또는 handleTextMessage가 실행됩니다.

 

소스를 완성해보겠습니다.

 

SocketHandler.java

package com.psw.chating.handler;

import java.util.HashMap;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
public class SocketHandler extends TextWebSocketHandler {
	
	HashMap<String, WebSocketSession> sessionMap = new HashMap<>(); //웹소켓 세션을 담아둘 맵
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
		String msg = message.getPayload();
		for(String key : sessionMap.keySet()) {
			WebSocketSession wss = sessionMap.get(key);
			try {
				wss.sendMessage(new TextMessage(msg));
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
		super.afterConnectionEstablished(session);
		sessionMap.put(session.getId(), session);
	}
	
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		//소켓 종료
		sessionMap.remove(session.getId());
		super.afterConnectionClosed(session, status);
	}
}

 

 

 

구현체를 이제 등록해보겠습니다.

WebSocketConfig.java

package com.psw.chating.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import com.psw.chating.handler.SocketHandler;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{

	@Autowired
	SocketHandler socketHandler;
	
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(socketHandler, "/chating");
	}
}

먼저 생성하였던 구현체를 등록하는 부분입니다.

 

마지막으로 chat.jsp를 완성하고 메시지가 주고 받아지는지 확인해보겠습니다.

 

chat.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<meta charset="UTF-8">
	<title>Chating</title>
	<style>
		*{
			margin:0;
			padding:0;
		}
		.container{
			width: 500px;
			margin: 0 auto;
			padding: 25px
		}
		.container h1{
			text-align: left;
			padding: 5px 5px 5px 15px;
			color: #FFBB00;
			border-left: 3px solid #FFBB00;
			margin-bottom: 20px;
		}
		.chating{
			background-color: #000;
			width: 500px;
			height: 500px;
			overflow: auto;
		}
		.chating p{
			color: #fff;
			text-align: left;
		}
		input{
			width: 330px;
			height: 25px;
		}
		#yourMsg{
			display: none;
		}
	</style>
</head>

<script type="text/javascript">
	var ws;

	function wsOpen(){
		ws = new WebSocket("ws://" + location.host + "/chating");
		wsEvt();
	}
		
	function wsEvt() {
		ws.onopen = function(data){
			//소켓이 열리면 초기화 세팅하기
		}
		
		ws.onmessage = function(data) {
			var msg = data.data;
			if(msg != null && msg.trim() != ''){
				$("#chating").append("<p>" + msg + "</p>");
			}
		}

		document.addEventListener("keypress", function(e){
			if(e.keyCode == 13){ //enter press
				send();
			}
		});
	}

	function chatName(){
		var userName = $("#userName").val();
		if(userName == null || userName.trim() == ""){
			alert("사용자 이름을 입력해주세요.");
			$("#userName").focus();
		}else{
			wsOpen();
			$("#yourName").hide();
			$("#yourMsg").show();
		}
	}

	function send() {
		var uN = $("#userName").val();
		var msg = $("#chatting").val();
		ws.send(uN+" : "+msg);
		$('#chatting').val("");
	}
</script>
<body>
	<div id="container" class="container">
		<h1>채팅</h1>
		<div id="chating" class="chating">
		</div>
		
		<div id="yourName">
			<table class="inputTable">
				<tr>
					<th>사용자명</th>
					<th><input type="text" name="userName" id="userName"></th>
					<th><button onclick="chatName()" id="startBtn">이름 등록</button></th>
				</tr>
			</table>
		</div>
		<div id="yourMsg">
			<table class="inputTable">
				<tr>
					<th>메시지</th>
					<th><input id="chatting" placeholder="보내실 메시지를 입력하세요."></th>
					<th><button onclick="send()" id="sendBtn">보내기</button></th>
				</tr>
			</table>
		</div>
	</div>
</body>
</html>

 

동작 확인

3개의 브라우저 생성

부트 어플리케이션을 구동하고 localhost/chat으로 접근하면 위와같은 뷰를 확인 할수 있습니다.

혼자서 3개의 브라우저를 열고 사용자명을 입력 후 테스트를 해보았습니다.

채팅 테스트

 

다음장에는 상대방과 자신을 구분을 처리해보겠습니다.

반응형
반응형

https://myhappyman.tistory.com/86

 

Spring - 파일 다운로드 예제 - IE, Chrome (스프링 파일 다운로드 구성)

스프링 프로젝트에서 파일 업로드와 다운로드를 하는 방법을 알아보겠습니다. 이번 포스팅에선 다운로드 예제를 작성했습니다. 추가 메이븐 다운로드 없이 스프링 설정만 하시면 됩니다. 파일 다운로드 web.xml

myhappyman.tistory.com

이전 포스팅 스프링에서 파일 다운로드 받기에 이어 업로드 예제를 진행해보겠습니다.

 

다운로드는 위 URL의 포스팅을 참고해주세요.

 

 

 

파일 다운로드

servlet-context.xml

<!--  spring-upload -->
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />
<!--  spring-upload -->

multipartResolverbean등록하여 추가합니다.

 

web.xml

파일 전송시 서블릿에서 10메가로 파일제한 처리하였습니다.

<!-- Processes application requests -->
<servlet>
	<servlet-name>appServlet</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/servlet-context.xml</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
	<!-- fileupload multipart-config maxSize set -->
	<multipart-config>
        <max-file-size>104857600</max-file-size> <!-- 10MB limit -->
        <max-request-size>104857600</max-request-size>
        <file-size-threshold>0</file-size-threshold>
    </multipart-config>
    <!-- fileupload multipart-config -->
</servlet>

 

commonController.java

upload요청이 올 경우 처리할 컨트롤러를 추가합니다.

(common/upload매핑)

import java.io.File;
import java.io.IOException;
import java.util.HashMap;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/common")
public class CommonController{
	
	private static final String FILE_SERVER_PATH = "C:/test";

	@RequestMapping("/upload")
	public String upload(@RequestParam("uploadFile") MultipartFile file, ModelAndView mv, Model model) throws IllegalStateException, IOException {
		if(!file.getOriginalFilename().isEmpty()) {
			file.transferTo(new File(FILE_SERVER_PATH, file.getOriginalFilename()));
			model.addAttribute("msg", "File uploaded successfully.");
		}else {
			model.addAttribute("msg", "Please select a valid mediaFile..");
		}
		
		return "board/board";
	}
}

uploadFile로 요청이 파라미터 값을 확인하고 지정한 경로로 저장합니다.

(C:/test)

 

board.jsp

<%@page import="java.net.URLEncoder"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
  <head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<meta name="description" content="">
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>파일 다운로드</title>
  </head>
  <body>
  	<div class="content">
		<br/><br/>
		<h3>파일 업로드</h3>
		<form action="/common/upload.do" method="post" enctype="multipart/form-data">
		  <table>
		    <tr>
		      <td>Select File</td>
		      <td><input type="file" name="uploadFile" /></td>
		      <td>
		        <button type="submit">Upload</button>
		      </td>
		    </tr>
		  </table>
		</form>
	</div>
  </body>
</html>

ui를 작성합니다.

 

파일을 선택하고 submit버튼을 누르면 action 값에 의해 만들어 놓은 컨트롤러로 전달이 됩니다.

 

 


동작 결과

Upload버튼 클릭 후 정상적으로 서버에 저장되는 모습

 

c:/test에 저장된 모습

반응형
반응형

스프링 프로젝트에서 파일 업로드와 다운로드를 하는 방법을 알아보겠습니다.

 

이번 포스팅에선 다운로드 예제를 작성했습니다.

 

추가 메이븐 다운로드 없이 스프링 설정만 하시면 됩니다.

 

 

 

파일 다운로드

web.xml

<!-- Filter -->
<filter>
	<filter-name>encodingFilter</filter-name>
	<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
	<init-param>
		<param-name>encoding</param-name>
		<param-value>utf-8</param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name>encodingFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Filter -->


<!-- Processes application requests -->
<servlet>
	<servlet-name>appServlet</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/servlet-context.xml</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>

먼저 web.xml에서 필터 설정과 DispatcherServlet상태를 확인하겠습니다.

필터를 통해 utf-8 인코딩 처리를 하였고 servlet-context.xml의 위치는 /WEB-INF/spring/servlet-context.xml 로 지정하였습니다.

 

servel-context.xml

<!-- spring-donwload bean -->
<beans:bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
    <beans:property name="order" value="0" />
</beans:bean>
<beans:bean id="downloadView" class="com.psw.myapp.utils.DownloadView" />
<!-- /spring-donwload bean -->

<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	<beans:property name="prefix" value="/WEB-INF/views/" />
	<beans:property name="suffix" value=".jsp" />
	<beans:property name="order" value="1" />
</beans:bean>

InternalResourceViewResolverorder순서를 1로 변경하였습니다.

DonwloadView 자바파일에서 다운로드 처리를 진행할 것이며 bean등록 처리를 해기 위해 BeanNameViewResolver를 사용했습니다.

serlet-context.xml 위치

 

 

CommonController.java

import java.io.File;
import java.io.IOException;
import java.util.HashMap;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/common")
public class CommonController{
	
	private static final String FILE_SERVER_PATH = "C:/test";

	@RequestMapping("/download")
	public ModelAndView download(@RequestParam HashMap<Object, Object> params, ModelAndView mv) {
		String fileName = (String) params.get("fileName");
		String fullPath = FILE_SERVER_PATH + "/" + fileName;
		File file = new File(fullPath);
		
		mv.setViewName("downloadView");
		mv.addObject("downloadFile", file);
		return mv;
	}
}

모든 파일은 C:/test에 있다고 가정하였습니다.

파일은 전달받은 fileName 파라미터값을 통해 파일을 세팅합니다.

downloadFile에 다운로드할 파일을 처리하고 bean등록을한 downloadView로 넘길것입니다.

 

 

DownloadView.java

import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Map;

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

import org.springframework.util.FileCopyUtils;
import org.springframework.web.servlet.view.AbstractView;

public class DownloadView extends AbstractView {

	@Override
	protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception {

		File file = (File)model.get("downloadFile");
        if(file != null) {
            String fileName = null;
            String userAgent = request.getHeader("User-Agent");
            
            if(userAgent.indexOf("MSIE") > -1 || userAgent.indexOf("Trident") > -1){
                fileName = URLEncoder.encode(file.getName(), "utf-8").replaceAll("\\+", "%20");;
            }else if(userAgent.indexOf("Chrome") > -1) {
            	StringBuffer sb = new StringBuffer();
            	for(int i=0; i<file.getName().length(); i++) {
            		char c = file.getName().charAt(i);
            		if(c > '~') {
            			sb.append(URLEncoder.encode(""+c, "UTF-8"));
            		}else {
            			sb.append(c);
            		}
            	}
            	fileName = sb.toString();
            }else {
            	fileName = new String(file.getName().getBytes("utf-8"));
            }
            response.setContentType(getContentType());
            response.setContentLength((int)file.length());
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\";");
            response.setHeader("Content-Transfer-Encoding", "binary");
            
            OutputStream out = response.getOutputStream();
            FileInputStream fis = null;
            try {
                fis = new FileInputStream(file);
                FileCopyUtils.copy(fis, out);
            } catch(Exception e){
                e.printStackTrace();
            }finally{
                if(fis != null){
                    try{
                        fis.close();
                    }catch(Exception e){
                    	e.printStackTrace();
                    }
                }
                
                if(out != null) {
                	out.flush();
                }
            }
            
        }
	}
}

donwloadView는 AbstractView를 상속받아 사용합니다.

AbstractViewView구현을 위한 추상클래스로 컨트롤러가 반환한 모델과 병합되는 뷰입니다.

 

앞서 CommonController.java에서 세팅한 file인 downloadFile을 받아서 ie인지 체크 후 헤더 세팅과 OutputStream을 통해 파일을 출력합니다.

 

또한, 주의사항으로 브라우저마다의 인코딩이 달라서 User-Agent를 통해 접속한 브라우저를 체크하고 출력해줄 파일명을 만들때 인코딩 방식을 다르게 해야합니다. IE의 경우엔 11버전부턴 식별문자가 Trident로 변경되어 MSIE와 같이 조건문에 추가하였습니다.

 

controller와 downloadView 위치

 

이제 다운로드가 정상적으로 되는지 테스트 해보겠습니다.

전 board.jsp라는곳에서 처리할 예정이므로 컨트롤러와 jsp를 추가로 만들겠습니다.

 

 

BoardController.java

import java.util.HashMap;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/board")
public class BoardController {
	
	@RequestMapping("/board.do")
	public ModelAndView board(@RequestParam HashMap<Object, Object> params, ModelAndView mv) {
		mv.setViewName("board/board");
		return mv;
	}
}

board/board.do url로 요청이 들어오면 board/board.jsp를 호출하도록 컨트롤러 설정을 했습니다.

 

 

board.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
  <head>
	<meta charset="UTF-8">
	<meta name="description" content="">
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>파일 다운로드</title>
  </head>
  <body>
  	<div class="content">
		<br/><br/>
		<h3>파일 다운로드</h3>
		<a href="/common/download.do?fileName=이미지예제.jpg">download</a>
	</div>
  </body>
  
</html>

board.jsp

이제 마지막으로 WEB-INF/views/board 디렉토리와 board.jsp파일을 만들고 board.jsp를 작성했습니다.

 

board.jsp

a태그를 클릭하시면 CommonContorller에 요청을 했다가 DownloadView를 통해 다운로드가 되는 것을 볼 수 있습니다.

 

 

 

c:/test/이미지예제.jpg

 

다운로드된 jpg

 

 

 

IE 인코딩 에러

 

예제를 그대로 진행하면 한글명때문에 익스에서 하시는분들은 에러가 발생할 수 있습니다.

IE 예외처리 인코딩을 위해 jsp부분을 수정해주시면 됩니다.

 

 

board.jsp

<%@page import="java.net.URLEncoder"%>

...

<a href="/common/download.do?fileName=<%=URLEncoder.encode("이미지예제.jpg")%>">download</a>

URLEncoder를 추가 import해주시고 파라미터부분을 인코딩하여 넘기시면 IE에서도 되는 걸 확인 할 수 있습니다.

 

IE에서 정상 다운로드 처리

 

 

 

파일 업로드는 아래 URL에서 진행됩니다.

https://myhappyman.tistory.com/89

 

Spring - 파일 업로드 예제

https://myhappyman.tistory.com/86 Spring - 파일 다운로드 예제 - IE, Chrome (스프링 파일 다운로드 구성) 스프링 프로젝트에서 파일 업로드와 다운로드를 하는 방법을 알아보겠습니다. 이번 포스팅에선 다운로..

myhappyman.tistory.com

 

반응형
반응형

Spring tiles에 대해 다뤄보기 위해 새로운 프로젝트인 Legacy 프로젝트를 생성하고 파일들을 만들고

위치에 따라 설정을 하고 있었는데 'org.springframework.web.servlet.view.tiles3.TilesConfigurer' not found 와 같은 에러가 발생하였다.

 

관련 검색을 해보니 스프링 프레임워크의 버전이 낮아서 발생한 문제였다.

해결 방법으로 버전을 올린 뒤 Maven update - project clean하여 해결하였다.

 

 

기존에 문제가 되던 springframework 버전

3.1.1.RELEASE에서는 tiles3를 사용할 수 없었다.

 

확인을 해보니 3.2이상으로 버전을 올려야 한다고 해서 3.2.18로 변경하였다.

3.2.18.RELEASE로 변경해주었다.

project 우클릭 후 MAVEN - Update project... 처리 후 Proejct - clean... 이후에 에러가 사라진걸 볼 수 있었다.

 

사용한 tiles 버전은 3.0.3이였다.

반응형
반응형

스프링 타일즈란 뷰페이지의 jsp들을 상단, 사이드, 메인, 하단을 설정 상태로 include 처리해주는 구조의 템플릿을 말합니다.

 

페이지들을 일괄관리 할 수 있고, 공통사용하는 부분들을 매번 등록을 따로 해주지 않아도 되기 때문에 편리합니다.

 

적용하는법에 대해 알아보겠습니다.

먼저, 타일즈 적용을 위해 pom.xml에 메이븐 추가부터 해보겠습니다.

 


 

pom.xml - 라이브러리 추가

<properties>
	<org.apache.tiles-version>3.0.3</org.apache.tiles-version>
</properties>

<!-- Tiles -->
<dependency>
	<groupId>org.apache.tiles</groupId>
	<artifactId>tiles-core</artifactId>
	<version>${org.apache.tiles-version}</version>
</dependency>

<dependency>
	<groupId>org.apache.tiles</groupId>
	<artifactId>tiles-servlet</artifactId>
	<version>${org.apache.tiles-version}</version>
</dependency>

<dependency>
          <groupId>org.apache.tiles</groupId>
          <artifactId>tiles-jsp</artifactId>
          <version>${org.apache.tiles-version}</version>
      </dependency>

<dependency>
	<groupId>org.apache.tiles</groupId>
	<artifactId>tiles-extras</artifactId>
	<version>${org.apache.tiles-version}</version>
</dependency>

Maven에 추가된 모습

pom.xml을 열어서 원하는 버전으로 입력하시면 다운로드가 될 겁니다.

저는 3.0.3버전으로 진행했습니다.

 

버전 정보들은 아래 URL에서 확인하시면 좋을 것 같습니다.

https://mvnrepository.com/artifact/org.apache.tiles/tiles-jsp

 

Maven Repository: org.apache.tiles » tiles-jsp

Tiles JSP support: Classes and tag libraries to use Tiles in a JSP environment. VersionRepositoryUsagesDate3.0.x3.0.8Central8Sep, 20173.0.7Central12Aug, 20163.0.5Central18Sep, 20143.0.4Central5May, 20143.0.3Central4Nov, 20133.0.1Central7Jul, 20123.0.0Centr

mvnrepository.com

 

다음으로 사용할 상단, 사이드, 메인, 하단으로 사용할 jsp들을 만들어 놓겠습니다.

나중에 타일즈 설정파일에서 연결을 할겁니다.

 

WEB-INF/views/tiles라는 디렉토리를 만들고 안에 각각 JSP들을 구성합니다.

 

 

사용할 JSP구성

JSP 4개 생성

안에 사용할 내용은 비워두셔도 됩니다.

 

다음은 servlet-context.xmlInternalResourceViewResolver를 변경 및 tiles설정을 추가하겠습니다.

 

 

servlet-context.xml

<beans:bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <beans:property name="prefix" value="/WEB-INF/views/" />
    <beans:property name="suffix" value=".jsp" />
    <beans:property name="order" value="2" />
</beans:bean>

<!-- tiles 설정 -->
<beans:bean id="tilesViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <beans:property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView" />
    <beans:property name="order" value="1" />
</beans:bean>
 
<beans:bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
    <beans:property name="definitions">
        <beans:list>
            <beans:value>/WEB-INF/spring/tiles-define.xml</beans:value>
        </beans:list>
    </beans:property>
</beans:bean>

order 부분을 유의해서 설정해주셔야 합니다. tiles가 우선순위가 되도록 해주어야 합니다.

 

 

tiles 설정을 추가 후 이클립스에서 에러가 발생한다면 아래 포스팅을 참고해주세요

https://myhappyman.tistory.com/82

 

Spring - tiles 적용 에러 발생 'org.springframework.web.servlet.view.tiles3.TilesConfigurer' not found

Spring tiles에 대해 다뤄보기 위해 새로운 프로젝트인 Legacy 프로젝트를 생성하고 파일들을 만들고 위치에 따라 설정을 하고 있었는데 'org.springframework.web.servlet.view.tiles3.TilesConfigurer' not foun..

myhappyman.tistory.com

 

servlet-context.xml을 추가했으면 beans:value에 처리한 tiles-define.xml을 생성하고 설정하겠습니다.

tiles-define.xml

xml을 생성하고 설정을 하겠습니다.

 

 

tiles-define.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
 
<tiles-definitions>
	<!-- main layout -->
	<definition name="layout-tiles" template="/WEB-INF/views/tiles/tiles-layout.jsp">
		<put-attribute name="header" value="/WEB-INF/views/tiles/headerTemplate.jsp" />
		<put-attribute name="left" value="/WEB-INF/views/tiles/sidebarTemplate.jsp" />
		<put-attribute name="body" value="" />
		<put-attribute name="foot" value="/WEB-INF/views/tiles/footerTemplate.jsp" />
	</definition>
	
	<definition name="*/*" extends="layout-tiles">
		<put-attribute name="body" value="/WEB-INF/views/{1}/{2}.jsp" />
		<put-attribute name="title" value="게시판" />
	</definition>
</tiles-definitions>

 

이 부분에서는 상단, 사이드, 바닥 메인으로 사용할 템플릿 등을 설정합니다.

definition에서 name은 변수처럼 사용할 이름을 지정하고 template은 사용할 jsp를 지정합니다.

그 내부에는 따라오는 헤더, 사이드, 바닥 jsp들을 처리하고 마찬가지로 name은 사용할 이름 value에는 실제로 들어갈 jsp를 입력합니다.

 

먼저 생성해놓은 JSP 중 tiles-layout.jsp를 메인으로 사용하고 각각 header sidebar, footer부분을 영역별로 사용할 예정입니다.

 

아래서 extends부분은 위에서 선언한 jsp를 상속받아 실제로 사용하는 페이지에 이식하고 body영역은 실제 페이지를 사용하기 위해 설정하는 부분으로 메인 body로 쓸 jsp영역을 설정할 수 있으며, {1}/{2}.jsp을 통해 board/board.jsp로 갈때 자연스럽게 이식되어 사용할 수 있게 합니다.

title과 같은 설정으로 브라우저의 title태그도 변경이 가능합니다.

 

이로써 간단하게 tiles설정이 끝났습니다.

 


 

JSP들 소스 수정과 CSS들을 입력하여 동작하는지 확인해보겠습니다.

 

headerTemplate.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<div class="Header">Header</div>

 

sidebarTemplate.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<div class="SideBar">Side</div>

 

footerTemplate.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<div class="Footer">Footer</div>

 

tiles-layout.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"  %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<!-- 공통변수 처리 -->
<c:set var="CONTEXT_PATH" value="${pageContext.request.contextPath}" scope="application"/>
<c:set var="RESOURCES_PATH" value="${CONTEXT_PATH}/resources" scope="application"/>
<!DOCTYPE html>
<html>
  <head>
	<meta charset="UTF-8">
	<meta name="description" content="">
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
	<script type="text/javascript">
		var CONTEXT_PATH = "${CONTEXT_PATH}";
		var RESOURCES_PATH = "${RESOURCES_PATH}";
	</script>
	<link rel="stylesheet" href="${RESOURCES_PATH}/css/common.css">
    <title><tiles:insertAttribute name="title" /></title>
  </head>
  
  <body>
  	<div class='wrap'>
  		<tiles:insertAttribute name="header" />
		  <div class='content'>  	
  			<tiles:insertAttribute name="left"/>
	  		<div class="page_content">
	  			<tiles:insertAttribute name="body"/>
	  		</div>
  		</div>
  		<tiles:insertAttribute name="foot" />
  	</div>
  </body>
  
</html>

공통변수 처리부분이나 script부분은 무시하셔도 됩니다.

tiles-define.xml에서 각각 JSP들을 불러왔으니 사용해야겠죠? 해당페이지에서 원하는 영역에 삽입을 할 수 있습니다.

<tiles:insertAttribute name="사용할 이름" />을 통해 적용하시면 됩니다.

 

resources/css/common.css를 만들었습니다.

 

 

common.css

@charset "UTF-8";

*{
	margin: 0;
	padding: 0;
}
.wrap{
	width: 100%;
}

.Header, .content, .Footer{
	width: 100%;
	float: left;
}

.Header, .Footer{
	height: 8em;
}

.SideBar{
	width: 10%;
	height: 600px;
	background-color: #FFBB00;
	float: left;
}

.page_content{
	width: 90%;
	height: 600px;
	background-color: #EAEAEA;
	float: left;
}

.Header{
	background-color: #ABF200;
}

.Footer{
	background-color: #FF00DD;
}

 

 

예시로 board/board.jsp를 이동하게 된 경우 발생한 결과 화면입니다.

tiles 적용

반응형
반응형

자세한 mybatis mysql 연결 설정법은 이번 글에서 포스팅하지 않겠습니다.

일반적으로 mybatis를 통해 연결하는게 아닌 터널링을 통해 접근하는법을 포스팅입니다.

 

@WebListener 어노테이션은 WAS서버에게 해당 클래스가 리스너임을 명시해줍니다.

 

ServletContextListener 클래스를 상속받으며 인터페이스를 상속받았으므로 2개의 클래스를 재정의하여

사용하시면 됩니다.

 

contextInitialized, contextDestroyed 메소드가 존재합니다.

 

contextInitialized : 해당 메소드는 was가 시작되면서 dispatcherServlet보다 먼저 시작되는 메소드입니다.

contextDestroyed : 해당 메소드는 was가 종료되면 종료되기 직전에 실행되는 메소드입니다.

 

@WebListener를 시작하기전 pom.xml에 라이브러리를 추가합니다.

 

pom.xml

<!-- WebListnener annotation-->
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>4.0.1</version>
  <scope>provided</scope>
</dependency>

<!-- jsch 터널링을 위해 필요합니다. -->
<dependency>
  <groupId>com.jcraft</groupId>
  <artifactId>jsch</artifactId>
  <version>0.1.53</version>
</dependency>

 

SSH 연결 class를 정의합니다.

SSHConnection.java

import java.util.Properties;

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;

public class SSHConnection {
	private final static String HOST = "SSH연결을 할 IP";
	private final static Integer PORT = 포트; //기본포트는 22
	private final static String SSH_USER = "연결 USER값"; //ex) root
	private final static String SSH_PW = "연결 비밀번호값"; //ex) 1234
	
	private Session session;
	
	public void closeSSH() {
		session.disconnect();
	}
	
	public SSHConnection() {
		try {
			Properties config = new Properties();
			config.put("StrictHostKeyChecking", "no");
			JSch jsch = new JSch();
			session = jsch.getSession(SSH_USER, HOST, PORT);
			session.setPassword(SSH_PW);
			session.setConfig(config);
			session.connect();
			session.setPortForwardingL(3316, "127.0.0.1", 3306); //127.0.0.1/3316으로 접근한 포트를 연결HOST/3306으로 포트포워딩
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
}

 

 

위에서 정의한 클래스를 사용하는 클래스를 정의합니다.

MyContextListener.java @WebListener

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class MyContextListener implements ServletContextListener {
	
	private SSHConnection sshConnection;

	@Override
	public void contextInitialized(ServletContextEvent sce) {
		System.out.println("init start!");
		try {
			sshConnection = new SSHConnection();
		}catch(Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void contextDestroyed(ServletContextEvent sce) {
		System.out.println("init destory!");
		sshConnection.closeSSH();
	}
}

 

contextInitialized 메소드는 was가 동작하면 바로 실행되면서 앞 서 만든 SSHConnection 클래스를 통해 터널링을 할 준비를 합니다.

 

mybatis정보 (mybatis-context.xml)

<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close" >
  <property name="driverClassName" value="com.mysql.jdbc.Driver" />
  <property name="url" value="jdbc:mysql://127.0.0.1:3316/databaseName" />
  <property name="username" value="username" />
  <property name="password" value="password" />
  <property name="validationQuery" value="SELECT 1 FROM DUAL" />
  <property name="testWhileIdle" value="true" />
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">     
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations" value="classpath:mapper/**/*.xml" />
  <property name="configLocation" value="/WEB-INF/mybatis/mybatis-config.xml" />
  <property name="transactionFactory">
  	<bean class="org.apache.ibatis.transaction.managed.ManagedTransactionFactory" />
  </property>
</bean>

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
	<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>

3316으로 접근합니다.

하지만 터널링 처리로 인해 3306의 SSH 연결한 3306의 mysql에서 데이터를 가져와서 데이터를 뿌려줍니다.

 

 

결과페이지

local에서 was를 돌렸지만 SSH 터널링으로 인해 연결한 120서버(예시)의 mysql 3306포트의 데이터를 조회하여 결과를 가져왔습니다.

login.jsp

 

 

page 결과

반응형