v55_1 -> JavaEE의 Servlet 컨테이너 적용하기
// Java Enterprise Edition // 일반 개발할때 사용하는 기술
// Java Standard Edition // Dsektop App 개발 기술
// Java Micro Edition // IoT(Embedded) 개발 기술
JavaEE와 Servlet 컨테이너와의 관계이다. // JavaEE를 확인하고 Servlet 버전을 확인하자.
즉, 버전에 맞는 Servlet 메소드를 사용하여야 한다. // 회사마다 다르다. // 학원에선 9버전 사용
JavaEE 5 // 2006년 // Tomcat 6 // Servlet 2.5 // JSP 2.1 // EJB 3.0
JavaEE 6 // 2009년 // Tomcat 7 // Servlet 3.0 // JSP 2.2 // EJB 3.1
JavaEE 7 // 2013년 // Tomcat 8.5 // Servlet 3.1 // JSP 2.3 // EJB 3.2
JavaEE 8 // 2017년 // Tomcat 9 // Servlet 4.0 // JSP 2.3 // EJB 3.2
// http://tomcat.apache.org/// 왼쪽 Download에서 버전 클릭 // Binary Distributions // Core // zip 다운
// Source Code Distributions // zip 다운 // User 밑에 server폴더 생성 // 다운받은 zip server 폴더 밑에 압출풀기
// tomcat 포트 변경 //
// VSC로 tomcat 폴더 가져오기 // conf - server.xml 열기 // Connector 태그 port 원하는 값으로 변경
<Connector port="9999" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
// tomcat 관리자 아이디 등록 //
// conf - tomcat-users.xml // role 주석 풀기 // rolename 변경
<role rolename="tomcat"/>
<role rolename="manager-gui"/>
<user username="tomcat" password="1111" roles="tomcat, manager-gui"/>
// conf - Catalina - localhost 폴더 생성 // manager.xml 파일 생성
<?xml version="1.0" encoding="UTF-8"?>
<Context privileged="true" antiResourceLocking="false"
docBase="${catalina.home}/webapps/manager">
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="^.*$" />
</Context>
// 톰캣 서버 실행 //
// bin - startup.bat 실행 // bat가 window, sh는 unix에서 사용하는 파일이다.
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'javax.servlet' 검색
// org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// ContextLoaderListener 변경 // ServletContextListener를 구현하도록 한다.
// @WebListener 애노테이션을 붙혀, 서블릿 컨테이너가 관리하게끔 한다.
// ServletContextListener는 ApplicationContextListener처럼 context에 객체를 담아 관리하는 목적이다.
// Servlet에 RequestMapping 애노테이션을 붙혀 IoC 컨테이너가 객체관리를 했었다.
// 이제부터 Servlet은 ServletContextLisner가 관리한다.
// 따라서 RequestMappingHandlerMapping관련 코드가 필요가 없다.
// 기존에는 Context를 관리하는 ApplicationContextListener에 IoC 컨테이너가 들어가 있었다.
// 여기서 Servlet와 Context를 Servlet에서 관리하고, 그 외의 객체는 IoC 컨테이너가 관리한다.
// Context 폴더 삭제 // ApplicationContextListener를 사용하지 않기 때문에
// *Servelt 변경 //
//@Component
@WebServlet("/board/addForm")
public class BoardAddFormServlet extends GenericServlet {
private static final long serialVersionUID = 1L;
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
res.setContentType("text/html;charset=UTF-8");
PrintWriter out = res.getWriter();
// 이전에 작성해둔 html을 사용하려면 out객체가 필요한데, 파라미터로 받아 사용할 수 없다.
// 대신 파라미터로 받은 ServletResponse에게 PrintWriter를 받아 사용할 수 있다 // getWriter() 호출
// PrintWriter를 호출하기 전에, ContentType // 즉 어떤 언어를 사용해야 하는지 미리 알려줘야 한다.
// 또한 serialVersionUID를 설정하여야 한다 // 직렬화가 필요하다.
// BoardAddServlet 변경 // Service를 호출하는 Servlet들 예시 //
// 프로세스는 고객이 입력하고, 그 입력을 Servlet이 Service에게, Service가 Dao에게 전달했다.
// 기존에는 Servlet은 Service를 생성자로 받아 사용했는데, 이는 IoC 컨테이너를 사용하여,
// 생성자에 필요한 파라미터를 IoC 컨테이너에서 알아서 생성하여 넣어줬었다.
// 이제 Servlet와 IoC 컨테이너는 ServletContextLisner가, *Service 객체는 IoC컨테이너가 관리한다.
// Service를 호출하려면 ServletContext에서 IoC컨테이너를 꺼내고, IoC컨테이너에서 꺼내야 한다.
@WebServlet("/board/add")
public class BoardAddServlet extends GenericServlet {
private static final long serialVersionUID = 1L;
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
try {
res.setContentType("text/html;charset=UTF-8");
PrintWriter out = res.getWriter();
ServletContext servletContext = req.getServletContext();
ApplicationContext iocContainer =
(ApplicationContext) servletContext.getAttribute("iocContainer");
BoardService boardService = iocContainer.getBean(BoardService.class);
// ServerApp 삭제 // Tomcat Server를 사용할 것이기 때문에 필요가 없다.
// Tomcat Server로 기존의 만든 자바 class 파일들을 가져가기 위해 gradle을 사용한다.
// 1. war 플러그인 추가 // 웹 애플리케이션 배치 파일을 생성하기 위함.
// build.gradle // plugins // id 'war' 추가 // $ gradle bulid // server 경로에서 실행
// war를 플러그인에 추가하고 gradle build를 하여야 한다. // 안하면 war파일이 생성되지 않는다.
// 생성된 war 파일을 tomcat 경로 // webapps 폴더 안에 넣고 서버 실행 //
// 톰캣 서버는 해당 war파일과 동일한 이름으로 폴더를 만들고 압축을 푼다.
// ex) webapp/bitcamp-project-server
// WebBrower에서 해당 Servlet 실행 //
// ex) board/list // http://localhost:9999/bitcamp-project-server/board/list // 이 경로로 들어가야 한다.
// eclipse에서 tomcat server 실행하는 방법 //
// preferences - Server - Runtime Encironments // add
// C:\Users\heusw\server\apache-tomcat-9.0.33 // 경로입력
// 웹애플리케이션 폴더 구조 //
// 1. Web-INF // 웹 App의 설정 파일 두는 폴더
// Web.xml // 설정파일, Deployment Description File(DD File)
// classes // Java class file(.class)이나 실행관련파일(.xml, .properties)을 둔다.
// lib // 의존 라이브러리를 둔다(.jar)
// 추가적으로 HTML, CSS, JavaScript, GIF 등 정적 웹 자원을 둔다.
// 2. JSP 파일 // 추후 자세히 설명
// 3. 기타 폴더
// 웹애플리케이션과 URL //
// 웹애플리케이션/a.html // ex) http://localhost:9999/myweb/a.html
// 웹애플리케이션/aaa/b.html // ex) http://localhost:9999/myweb/aaa/b.html
// http://서버주소:포트번호/웹애플리케이션폴더명/자원의경로
// 웹애플리케이션폴더명 // 별도로 별명이 부여되어있다면 별명을 사용한다.
// 자원의 경로 // resource path // 파일의 경로나 서블릿 경로를 말함
// 파일의 경로 // 실제 폴더나 파일이 존재 // 서블릿 경로 // servlet path // 가상의 경로를 말한다.
// ex) http://localhost:9999/myweb/aaa/bbb/c.html // myweb/aaa/bbb/c.html이 resource path이다.
// myweb/board/list // 만약 뒤가 이렇게 servlet path를 가리키기도 한다.
// '/'의 의미 //
// server root // 포트번호와 웹애플리케이션폴더명 사이의 '/'
// web app root (=context root) // 웹애플리케이션폴더명과 자원의 경로 사이의 '/'
// ex) <a href="/board/list"> </a> // board 앞에 '/'는 server root를 의미한다.
// 웹 애플리케이션 배포 절차 //
// src/main/java/안의 파일은 WEB-INF/classes/.../에 파일(*.class)로 두어야 하고,
// src/main/resources/안의 파일은 WEB-INF/classes/.../에 파일(*.properties, *.xml)로 두어야 한다.
// 의존라이브러리는 lib/에 파일(*.jar)로 두어야 한다.
// 원래는 각각 해줘야 하는데, gradle build로 war파일을 만들어 넣어두면 된다.
// $톰캣서버(servlet container)/webapps/에 gradle로 만든 파일(war)을 넣어두고
// 서버를 실행시키면 알아서 압축이 풀린다. // 그러니깐 꼭 gradle 쓰자
v55_2 -> 이클립스 웹 프로젝트로 전환하기
// 이클립스IDE에 웹애플리케이션 테스트 환경 구축하기 //
// 1. Preferences - Server - Runtime Encironments // add
// C:\Users\heusw\server\apache-tomcat-9.0.33 // 경로입력 //
// 2. Server 뷰 / 테스트 서버 환경 추가
// Server view에서 보여주는 Server는 C:/Users/heusw/eclipse-workspace/Servers/밑에 실재한다.
// Tomcat v9.0 Server at localhost-config 폴더 내용은 톰캣서버/conf/* 밑에서 복사해온 것이다.
// 3. 테스트 서버의 배치 폴더 준비 // Tomcat의 경우
// 테스트 서버에 대해 최초로 실행할 때, 폴더가 생성된다. // 파일은 실행할 때마다 복사 해옴
// C:\Users\heusw\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0
// bin // 원본 서버의 bin 폴더 사용
// conf // 2번에서 설명한 그 conf이다. // 톰캣서버/conf/* 밑에서 복사해온 것
// lib // 원본 서버의 lib 폴더 사용
// logs // 테스트 서버의 로그 파일
// tmp // 테스트 서버에서 실행중에 중간 작업물을 보관할 용도로 사용
// webapps // 테스트 서버에서 사용하지 않는다.
// work // JSP를 서블릿으로 변환한 자바파일을 두는 폴더
// wtpwebapps // 테스트 서버에 배치하는 애플리케이션을 두는 폴더 // 웹애플리케이션 폴더
// 자동배치 //
// 프로젝트 // src/main/java(*.java) /resource(*properties, *.xml) /webapp(*.html, *.css, *js, *.gif, *.jsp)
// /webapp/WEB-INF/(* 기타)
// 배치폴더 // tmp0/wtpwebapps/.../WEB-INF/classes // *.java 파일
// tmp0/wtpwebapps/.../WEB-INF // *.html, *.css, *js, *.gif, *.jsp 파일
// tmp0/wtpwebapps/... // * 기타 파일
// 프로젝트 // Servers/Tomcat v9.0 Server at localhost-config/.../*.xml // Server view에 보이는 폴더
// 배치폴더 // tmp0/conf/ // *.xml 파일
// 이클립스에서 프로젝트를 변경하면, Eclipse-workspace 밑 배치폴더에 적용이 된다.
// 실제로 실행이 되는 것은 배치폴더의 내용이 실행된다.
// 즉, 실제 작성하는 위치와 실행하는 폴더의 위치가 다르다.
// 어차피 프로젝트에 변경하면 배치폴더에 적용되니깐 절대로 배치폴더를 건드리지는 말자.
// 개발자로써 알아야될 소양이기 때문에 짚고 넘어간 것이다. // 배치폴더를 건드리진 말자
// build.gradle 변경 //
// id 'eclipse-wtp' // 이클립스 플러그인 기능 + 이클립스 웹 프로젝트용 설정 파일을 생성
// id 'war' // 배치 관련 기능을 처리한다.
// 프로젝트 아이콘에 지구본 모양이 추가된다. // 빨간 느낌표가 뜨면 cleanEclipse eclipse 다시 해보자
// Server view - 마우스 오른쪽 클릭 - Add and Remove - Server Add - finish
// Server view - 마우스 오른쪽 클릭 - Publish - tmp0/wtpwebapps/bitcamp-project-server 폴더 확인
// 정적자원 폴더 추가하기 //
// src/main/webapp/ 생성
// eclipse 프로젝트 설정 파일을 생성한 후에 이 디렉토리를 만들면, 설정파일에 이 디렉토리 정보가 포함되지 않는다.
// 다시 'gradle eclipse'를 실행하여 설정파일을 만들자.
// src/main/webapp/index.html 파일 생성
v56_1 -> HttpServlet 클래스 상속
// XxxServlet 변경 // GenericServlet을 상속받아 구현했었는데, HttpServlet를 상속받아 구현하게 변경한다.
// XxxAddFormServlet을 GET으로 처리하게 XxxAddServlet doGet() 메서드로 옮긴다.
// Service() 메서드를 doPost()로 옮긴다.
// <form action='add' method='post'> // HTML코드 추가 // doPost()를 가지고 있는 Add, Update
// doGET메서드 다음에 POST메서드로 넘어갈 수 있게 설정해줘야 한다.
// Add와 Update의 경우 추가되어야 하는 data가 url에 보이면 안되기 때문에, POST 요청으로 실행(데이터를 담아 올 때)하고
// delete / Update, Add(data 입력할 폼 요청) // 번호나 입력할 폼 요청이기 때문에 GET으로 실행하도록 한다.
v56_2 -> Servlet을 이용하여 Spring IoC 컨테이너 준비하기
// ContextLoaderListener 변경 // Servlet으로 Spring IoC 컨테이너를 준비할 것이기 때문에
// @WebListener를 주석으로 막는다. // Listener가 작용하면 안된다.
// LoaderListener를 사용하는게 조금 더 현재 트렌드에 가깝다. // 밑에 적을 Servlet 방법이 비교적 구식
// 그러나 둘 다 실무에서 고르게 사용하기 때문에 둘 다 알아둬야 한다.
// lms.servlet.AppInitServlet 추가 // Servlet에서 Spring IoC 컨테이너를 준비하여 공유하게 한다.
// AppInitServlet // 다른 Servlet이 사용할 IoC 컨테이너를 준비하는 역할을 한다.
// static Logger logger = LogManager.getLogger(AppInitServlet.class); // 로그 출력하게 로그도 가져온다.
// loadOnStartup을 이용 웹 애플리케이션이 시작할 때 자동 생성되게 한다.
// @WebServlet 애노테이션에 loadOnStartup 속성을 추가한다.
// service(), doXXX 메서드는 오버라이딩 하면 안된다. // 클라이언트 요청을 처리하는 Servlet이 아니기 때문
// init() 오버라이딩 // init(ServletConfig config)를 오버라이딩 하지 않는 이유 //
// HttpServlet에 들어가서 메소드를 확인해보면 알겠지만 init(ServletConfig config)가 init()를 호출하기 때문
// getServletContext(); // ContextLoaderListener에서는 ServletContextEvent sce를 파라미터로 넘겨 받았지만,
// HttpServlet를 상속받아 구현한 AppInitServlet에서는 GenericServlet에 getServletContext() 메서드가 구현되어있다.
// 따라서 상속받은 클래스이기 때문에 메서드를 단순히 호출하기만 하면 된다.
// ContextLoaderListener는 ServletContextListener를 상속받아 만들어진 클래스라 파라미터로 받았어야 했던 것이다.
// load-on-startup 처리 방법 // 1. 애노테이션으로 처리하는 방법 // 2. DD File(web.xml)에 설정하는 방법
// @WebServlet(value="/AppInitServlet", loadOnStartup = 1) // 1. 애노테이션을 붙힌다. // 1번으로 객체를 생성한다.
// value를 붙히지 않으면 실행이 안되기 때문에, URL을 지정해줘야 한다. // 실행이 안돼서 그냥 붙히는 것이다.
// 객체를 생성하고 init를 호출하면, Spring IoC 컨테이너를 생성하고, ServletContext에 담아 보관하는 일을 한다.
// Spring IoC 컨테이너를 생성하면, Spring IoC 컨테이너가 이제 기타 객체들을 만들어 보관한다.
// src/main/webapp/WEB-INF/web.xml 생성 // 2. WEB-INF 폴더와 web.xml 파일을 생성한다.
<servlet>
<servlet-name>AppInitServlet</servlet-name>
<servlet-class>com.eomcs.lms.servlet.AppInitServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
// 만든 AppInitServlet을 등록하고 load-on-startup을 1로 설정한다. // servlet-mapping은 안만든다.
// 이 class는 client가 호출할 게 아니기 때문에 servlet-mapping을 만들지 않는다. // 만들면 안된다.
// 프로세스 // 1. 별도로 호출하지 않아도 load-on-startup으로 AppInitServlet을 만들게 한다.
// 2. Servlet에서 IoC 컨테이너 준비 등 초기에 필요한 작업을 담당하게 한다. // 또한 IoC 컨테이너 보관
// localhost:9999/bitcamp-project-server/AppInitServlet 접속 // 접속시 오류창의 내용은 이런 Servlet이 없다는 게 아니다.
// GET이나 POST 요청을 처리할 doGET, doPOST 메서드가 없다는 이야기이다. // 당연히 메서드 구현하지 않는다.
v56_3 -> redirect와 refresh 활용
// doPost() 메서드 변경 // add, update //
// response.sendRedirect("list"); // 작업을 완료한 후 다른 페이지를 가라고 클라이언트에게 URL을 보낸다.
// sendRedirect("경로"); // URL이 '/'로 시작하면 서버 루트를 의미한다. // '/'로 시작하지 않으면 상대 경로를 의미한다.
// 위에서는 '/'로 시작하지 않기 때문에 상대경로를 의미한다.
// 상대경로란 리다이렉트 메시지를 받기전의 URL(/bitcamp-project-server/board/add)를 기준으로
// 계산한 경로(/bitcamp-project-server/board/list)이다.
// 즉 /bitcamp-project-server/board/add에서 실행됐기 때문에, /bitcamp-project-server/board/까지는 가지고 있고,
// /bitcamp-project-server/board/경로에서 list를 실행하라. // /bitcamp-project-server/board/list라는 뜻이다.
// redirect의 단점은 바로 URL을 보내 페이지 이동을 시켜버리기 때문에,
// add, update가 실패했는지 정상적으로 처리 됐는지 메세지를 출력하지 않는다.
// ErrorServlet 추가 // 에러 발생시 ErrorServlet로 보내기 위해 만든다.
// xxxServlet 변경 //
if (boardService.delete(no) > 0) { // 삭제했다면,
response.sendRedirect("list");
} else {
response.sendRedirect("../error");
}
// list는 /bitcamp-project-server/board/delete와 같은 경로내에 있고 // /bitcamp-project-server/board/list
// error는 /bitcamp-project-server/board/delete보다 상위 경로에 있다. // /bitcamp-project-server/error
// 따라서 ../로 상위 폴더를 지정한다.
if (boardService.delete(no) > 0) {
response.sendRedirect("list");
} else {
request.getSession().setAttribute("errorMessage", "삭제할 게시물 번호가 유효하지 않습니다.");
request.getSession().setAttribute("url", "board/list");
response.sendRedirect("../error");
}
// XxxServlet 변경 // Session을 사용해서, 각 Servlet에서 발생한 오류를 ErrorServlet에게 넘기도록 한다.
// Session을 사용할 때 // 여러 절차로 이루어지는 한 작업을 수행할 때 // 여러 Servlet이 한가지 작업을 수행할 때,
// 트렌젝션을 수행하는 용도로 직접 Data를 공유한다.
v56_4 -> 포워딩과 인클루딩
if (boardService.update(board) > 0) {
response.sendRedirect("list");
} else {
throw new Exception("변경할 게시물 번호가 유효하지 않습니다.");
}
} catch (Exception e) {
request.setAttribute("error", e);
request.setAttribute("url", "list");
request.getRequestDispatcher("/error").forward(request, response);
}
// xxxServlet 변경 // Add, Update, delete 변경 // 등록&변경중 오류 발생시 ErrorServlet으로 무조건 포워딩하게끔 만든다.
// 기존에는 error servlet 경로로 redirect를 시켰다. // 주소창에서도 error로 출력된다.
// 그러나 변경 후는 주소창에서는 오류 발생한 명령어 그대로 남아있는데,
// error가 발생했기 때문에 error에게 처리하라고 포워딩을 하는 것이다.
// ErrorServlet 변경 // ((Exception) request.getAttribute("error")).getMessage());
// 기존에는 트렌젝션을 사용하는 redirect 여서 Session에 보관하였으나,
// 이제는 처리를 아예 별도 서블릿에게 맡기는, 포워딩으로 처리하여 HttpServletRequest에 보관을 한다.
// 원래 예외처리가 발생하면, ServletException(e) 와 같은 식으로 처리 하였었는데,
// new Exception("변경할 게시물 번호가 유효하지 않습니다."); // 오류 메세지를 받는 객체를 만들어 그 객체를 넘긴다.
Exception error = (Exception) request.getAttribute("error");
out.printf("<p>%s</p>", error.getMessage());
// ErrorServlet에서는 HttpServletRequest에서 오류객체를 꺼내고, error 객체에 들어있는 메세지를 꺼내 출력하게 한다.
// error 객체는, Exception class의 객체이며, Exception class는 Throwable class의 자식 클래스이다.
// Throwable에는 getMessage()라는 메서드가 구현되어 있으며, getMessage() 메서드는 처음 error 객체를 만들 때,
// 생성자로 넘겨주었던 "변경할 게시물 번호가 유효하지 않습니다."를 리턴한다.
// Throwable class는 인터페이스가 아니며, setMessage() 메서드는 없다 // 생성자로만 Message를 받는다.
// 또한 원래 포워딩시 doGet 메서드에서 넘겨준다면, doGet으로 받고, Post는 Post로 받아야 한다.
// 그러나 ErrorServlet은 doGet에서 넘어올 수도 // delete // doPost에서 넘어올 수도 있다. // add, update
// 따라서 ErrorServlet의 메서드 명을 service로 바꿔준다.
v56_5 -> HttpSession을 이용하여 로그인 로그아웃 처리하기.
// LoginServlet 변경 //
request.getSession().setAttribute("loginUser", member);
// member 정보를 불러왔다면, HttpSession에 담아 보관한다.
// HeaderServlet 변경 //
out.println(" <ul class='navbar-nav mr-auto'>");
out.println(" </ul>");
Member loginUser = (Member) request.getSession().getAttribute("loginUser");
if (loginUser != null) {
out.printf(" <span class='navbar-text'>%s</span>\n", //
loginUser.getName());
out.println(" <a href='../auth/logout' class='btn btn-success btn-sm'>로그아웃</a>");
} else {
out.println(" <a href='../auth/login' class='btn btn-success btn-sm'>로그인</a>");
}
out.println("</div>");
// navbar-nav mr-auto를 적용한다는 태그를 먼저 넣어주고 // 이전에는 navbar-nav 였다
// ul 밑에 HttpSession에 담겨있는 member 정보가 있다면, member이름과 로그아웃을 출력하게 변경한다.
// LogoutServlet 추가 //
@WebServlet("/auth/logout")
public class LogoutServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.getSession().invalidate();
response.sendRedirect("../index.html");
}
}
// LogoutServlet 이 호출되면, HttpSession을 무효화시키고, index 페이지로 보낸다.
// index가 /auth와 같은 선상에 있기 때문에 ../ 상위 폴더로 보낸 후 index로 이동시켜야 한다.
v56_6 -> Cookie를 활용하여 사용자 정보 보관
String email = request.getParameter("email");
String password = request.getParameter("password");
Cookie cookie = new Cookie("email", email);
cookie.setMaxAge(60 * 60 * 24 * 30);
response.addCookie(cookie);
Member member = memberService.get(email, password);
// doPost에서 cookie를 한 달 동안 유지되게 설정 한 후 client에게 보낸다. // client는 HDD에 보관한다.
String email = "";
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("email")) {
email = cookie.getValue();
break;
}
}
}
out.println("<h1>로그인</h1>");
out.println("<form action='login' method='post'>");
out.printf("이메일: <input name='email' type='email' value='%s'>\n", email);
out.println("<input type='checkbox' name='saveEmail'> 이메일 저장해두기<br>");
out.println("암호: <input name='password' type='password'><br>");
out.println("<button>로그인</button>");
out.println("</form>");
// doGet에서 email이라는 이름으로 저장된 Cookie를 찾아 있으면, email에 저장한다. // getCookies() 리턴값은 Cookie 배열이다.
// email 값이 있다면, email 값을 자동으로 넣는다..
// checkbox를 체크했으면 email을 저장하고, 안했다면 저장 안하게 하기 위해 checkbox를 추가한다.
// 위의 코드는 client가 체크박스에 체크를 했는지 안했는지를 doGet에서 doPost로 값 보내는 용도이다.
String email = request.getParameter("email");
String password = request.getParameter("password");
String saveEmail = request.getParameter("saveEmail");
Cookie cookie = new Cookie("email", email);
if (saveEmail != null) {
cookie.setMaxAge(60 * 60 * 24 * 30);
} else {
cookie.setMaxAge(0);
}
response.addCookie(cookie);
// doPost에서 Client가 CheckBox를 체크했는지 알기 위해, saveEmail을 꺼내 확인한다.
// 체크를 하지 않았다면, cookie를 삭제한다. // setMaxAge에 0을 넣으면 삭제된다.
v56_7 -> Filter를 사용하여 사용자 접근 제어
@WebFilter("/*")
public class AuthFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String servletPath = httpRequest.getServletPath();
if (servletPath.endsWith("add") || servletPath.endsWith("delete") || servletPath.endsWith("update")) {
Member loginUser = (Member) httpRequest.getSession().getAttribute("loginUser");
if (loginUser == null) {
httpResponse.sendRedirect("../auth/login");
return;
}
}
chain.doFilter(request, response);
}
// @WebFilter("/*") // 모든 경로((/*)에 Filter를 적용한다. // AuthFilter를 거치게 한다.
// add, delete, update로 끝날 때, loginUser정보가 없다면 login으로 이동시킨다.
v56_8 -> 파일 업로드 기능 추가
// Servlet 3.0에 추가된 멀티파트 데이터 처리 기능을 이용하여 처리한다.
out.println("<h1>회원 입력</h1>");
out.println("<form action='add' method='post' enctype='multipart/form-data'>");
out.println("이름: <input name='name' type='text'><br>");
out.println("이메일: <input name='email' type='email'><br>");
out.println("암호: <input name='password' type='password'><br>");
out.println("사진: <input name='photo' type='file'><br>");
out.println("전화: <input name='tel' type='tel'><br>");
out.println("<button>제출</button>");
out.println("</form>");
// MemberAddServlet 변경 // doGet에 enctype='multipart/form-data' 코드를 추가 // photo type='file' 로 변경
// 이 것을 적용해줘야 멀티파트를 사용할 준비가 된다.
// @MultipartConfig(maxFileSize = 10000000) // 애노테이션 추가까지 해야, 멀티파트가 적용된다.
Part photoPart = request.getPart("photo");
if (photoPart.getSize() > 0) {
String dirPath = getServletContext().getRealPath("/upload/member");
String filename = UUID.randomUUID().toString();
photoPart.write(dirPath + "/" + filename);
member.setPhoto(filename);
}
// doPost에서 file을 저장할 때 Part를 이용한다.
// getServletContext().getRealPath(실제경로) // 실제 사진파일을 저장할 경로를 준비한다.
// UUID.randomUUID().toString(); // 파일명이 겹치면 안되기 때문에, UUID의 ramdom 메서드를 이용하여 파일명을 만든다.
if (member != null) {
out.println("<form action='update' method='post' enctype='multipart/form-data'>");
out.printf("<img src='../upload/member/%s' height='600'><br>\n", member.getPhoto());
out.printf("번호: <input name='no' type='text' readonly value='%d'><br>\n", //
member.getNo());
out.println("암호: <input name='password' type='password'><br>");
out.printf("사진: <input name='photo' type='file'><br>\n", //
member.getPhoto());
out.println("<p><button>변경</button>");
out.printf("<a href='delete?no=%d'>삭제</a></p>\n", //
member.getNo());
out.println("</form>");
} else {
out.println("<p>해당 번호의 회원이 없습니다.</p>");
}
// MemberDetailServlet 변경 // <img src='../upload/member/%s' 로 img 경로를 지정한다. // 실제 파일이 있는 경로
// member.getPhoto()로 파일명을 불러온다 // height='600' // 크기를 지정한다.
// MemberUpdateSevlet 변경 // MemberAddServlet와 같이 변경한다.
out.println("사진: <input name='photo' type='file'><br>");
out.println("사진: <input name='photo' type='file'><br>");
out.println("사진: <input name='photo' type='file'><br>");
out.println("사진: <input name='photo' type='file'><br>");
out.println("사진: <input name='photo' type='file'><br>");
// PhotoBoardAddServlet 변경 // doGet에서 enctype='multipart/form-data' 설정하고, name 이름을 같은 것으로 변경한다.
Collection<Part> parts = request.getParts();
String dirPath = getServletContext().getRealPath("/upload/photoboard");
for (Part part : parts) {
if (!part.getName().equals("photo") || part.getSize() <= 0) {
continue;
}
String filename = UUID.randomUUID().toString();
part.write(dirPath + "/" + filename);
photoFiles.add(new PhotoFile().setFilepath(filename));
}
// doPost에서 getParts() // Part 배열로 받는다.
v57_1 -> JSP 적용하기
// JSP // java Servlet Page // 응답 HTML 출력 담당
// JSP는 HTML 양식을 그대로 가지고 있으면서, Java 코드를 작성할 수 있다. // HTML을 자동으로 out.print문을 붙혀서 출력해준다.
// Java 코드를 가지고 JSP에서 실행 시켜주는 것이 아니다. // 작성한 HTML 코드를 out.print를 붙혀 HTML에게 전달하는 역할이다.
<%@ page import="com.eomcs.lms.domain.Board"%>
<%@ page import="java.util.List"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"
trimDirectiveWhitespaces="true"%>
<jsp:include page="/header.jsp"/>
<h1>게시글(JSP2)</h1>
<a href='add'>새 글</a><br>
<table border='1'>
<tr>
<th>번호</th>
<th>제목</th>
<th>등록일</th>
<th>조회수</th>
</tr>
<%
List<Board> list = (List<Board>) request.getAttribute("list");
for(Board item : list) {
%>
<tr>
<td><%=item.getNo()%></td>
<td><a href='detail?no=<%=item.getNo()%>'>=> <%=item.getTitle()%></a></td>
<td><%=item.getDate()%></td>
<td><%=item.getViewCount()%></td>
</tr>
<%
}
%>
</table>
<jsp:include page="/footer.jsp"/>
// <%@ page import="com.eomcs.lms.domain.Board"%> // import 방식은 이렇게 선언한다
// trimDirectiveWhitespaces="true"%> // trim은 불필요한 enter들을 다 제거해준다.
// Java 코드를 삽입할 때에는 <% %> 사이에 집어 넣으면 된다. // 값을 집어 넣을때는 공백이 없어야 한다.
// ex)<%=item.getDate()%> // %>와 Java코드가 딱 붙어있다 (O) //<%=item.getDate() %> // 한 칸 떨어져있다 (X) // 실행안댐.
// 모든 jsp 파일에 공통적으로 stylesheet와 script 코드 등 을 작성하지 않기 위해, header.jsp와 footer.jsp로 구분한다.
// header.jsp와 footer.jsp를 include로 처리한다. // header는 본문 시작 하기 전에 넣어야 한다.
// getbootstrap.com 접속 // getStarted 클릭 // 보통 CSS는 </head> 앞에, JS는 </body> 앞에 넣는다.
// 클래스와 상관 없이 필터를 삽입하고 싶을 때에는 AOP 기술을 사용한다.
// JSP와 Page Controller를 사용전, 필터를 삽입 할 때는 Interceptor 작성 기술을 사용한다.
// 1. 요청 핸들러 정의 2. 파라미터 선언 3. 값 리턴 4. 의존객체 주입 // 작성 방법
// 서블릿 전체나, 특정 서블릿에 대해 필터를 사용하고 싶을 때는 ServletFilter를 사용한다.
v57_2 -> JSP에 EL 적용하기
<%@ page import="com.eomcs.lms.domain.Board"%>
<%@ page import="java.util.List"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"
trimDirectiveWhitespaces="true"%>
<jsp:include page="/header.jsp"/>
<h1>게시글(JSP + EL)</h1>
<a href='add'>새 글</a><br>
<table border='1'>
<tr>
<th>번호</th>
<th>제목</th>
<th>등록일</th>
<th>조회수</th>
</tr>
<jsp:useBean id="list"
type="java.util.List<Board>"
class="java.util.ArrayList"
scope="request"/>
<%
for(Board item : list) {
pageContext.setAttribute("item", item);
%>
<tr>
<td>${item.no}</td>
<td><a href='detail?no=${item.no}'>=> ${item.title}</a></td>
<td>${item.date}</td>
<td>${item.viewCount}</td>
</tr>
<%
}
%>
</table>
<jsp:include page="/footer.jsp"/>
// pageContext에서 item을 꺼내, EL을 no, title 등 해당하는 항목들의 값을 꺼낸다.
v57_3 -> JSP에 JSTL 적용하기
<%@ page import="com.eomcs.lms.domain.Board"%>
<%@ page import="java.util.List"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"
trimDirectiveWhitespaces="true"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<jsp:include page="/header.jsp"/>
<h1>게시글(JSP + EL + JSTL)</h1>
<a href='add'>새 글</a><br>
<table border='1'>
<tr>
<th>번호</th>
<th>제목</th>
<th>등록일</th>
<th>조회수</th>
</tr>
<c:forEach items="${list}" var="item">
<tr>
<td>${item.no}</td>
<td><a href='detail?no=${item.no}'>=> ${item.title}</a></td>
<td>${item.date}</td>
<td>${item.viewCount}</td>
</tr>
</c:forEach>
</table>
<jsp:include page="/footer.jsp"/>
// JSP에 JSTL을 적용하여 자바 반복문을 c:forEach로 바꾸었다. // JSP에서 자바코드를 모두 제거하였다.
v58_1 -> Front Controller 설계 기법 적용하기
// Front Controller // 컨트롤러들의 공통 기능을 가져와서 통합 처리한다.
// 외부의 접점을 하나로 줄임으로써 요청을 제어하기가 쉬워진다.
// 원래는 Client의 요청에 따라 다이렉트로 Servlet에게 갔다면, Front Controller를 적용하여,
// Client의 요청이 Front Controller에게 모두 전달되게 한다.
// Front Controller에서 요청에 맞는 Servlet을 호출하도록 한다.
// DispatcherServlet 추가 // /app/* 요청을 처리한다.
// 모든 Servlet이 /app/을 거친 후에 요청을 받게 한다.
// Servlet이 직접 JSP를 인클루딩 하는 대신에, JSP URL을 ServletRequest에 저장한다.
// 직접 리다이렉트 하는 대신에, 리다이렉트 URL을 ServletRequest에 저장한다.
// 직접 예외처리 하는 서블릿으로 포워딩 하는 대신에, ServletRequest에 저장한다.
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String pathInfo = request.getPathInfo();
response.setContentType("text/html;charset=UTF-8");
ArrayList<Cookie> cookies = new ArrayList<>();
request.setAttribute("cookies", cookies);
request.getRequestDispatcher(pathInfo).include(request, response);
if (cookies.size() > 0) {
for (Cookie cookie : cookies) {
response.addCookie(cookie);
}
}
if (request.getAttribute("error") != null) {
Exception error = (Exception) request.getAttribute("error");
StringWriter out = new StringWriter();
error.printStackTrace(new PrintWriter(out));
request.setAttribute("errorDetail", out.toString());
request.getRequestDispatcher("/error.jsp").forward(request, response);
return;
}
String refreshUrl = (String) request.getAttribute("refreshUrl");
if (refreshUrl != null) {
response.setHeader("Refresh", refreshUrl);
}
String viewUrl = (String) request.getAttribute("viewUrl");
if (viewUrl.startsWith("redirect:")) {
response.sendRedirect(viewUrl.substring(9));
} else {
request.getRequestDispatcher(viewUrl).include(request, response);
}
}
// DispatchcerServlet에서 모든 요청을 처리해야 되기 때문에 Service 메서드를 오버라이딩 한다.
// request.getPathInfo() // 서블릿 경로(/app) 다음에 오는 자원의 경로를 알아낸다.
// getServletPath()는 서블릿 경로를 알려주는 것이라, /app을 리턴한다. // /app/자원의 경로로 변경을 하였기 때문에
// 서블렛 경로이기는 하지만 자원의 경로에 해당된다. // /app적용 이전에는 서블렛 경로였다.
// 서블릿을 인크루딩 하는 경우에, 쿠키 적용이 되지 않는다.
// SetAttribute("cookies", cookies); // 인클루딩 서블릿에서 쿠키 정보를 담을 수 있도록 리스트 객체를 준비
// request.getRequestDispatcher(pathInfo).include(request, response);
// Dispatcher에게 Servlet(자원의 경로)을 알려주고 include하여 Servlet을 실행시킨다.
// response.addCookie(cookie); // include 실행 후 쿠키가 있다면 쿠키를 응답헤더에 추가한다
// 그 후, Error 객체가 있으면 Error를 실행하고, refreshUrl을 전달했다면 refresh를 실행하고,
// 둘 다 null이라면, Servlet이 알려준 Url을 실행하도록 한다. // 단, Servlet에서 넘겨주는 Url은 redirect가 포함되어 있다.
// 따라서 redirect:를 제거한 나머지 Url을 사용하여야 한다.
boardService.add(board);
request.setAttribute("viewUrl", "redirect:list");
} catch (Exception e) {
request.setAttribute("error", e);
request.setAttribute("url", "list");
}
// 원래 담당하던 jsp 경로를 알려주던 코드와, Error 경로에게 보내는 코드를 삭제하고 Url을 담게만 한다.
v58_2 -> Page Controller를 POJO로 전환하고 Spring IoC 컨테이너에서 관리하기
// POJO(Plain Old Java Object) //
// Page Controller가 Servlet을 관리하게 하던 것을 IoC 컨테이너가 해당 객체를 관리하게 한다.
// Page Controller는 HttpServlet을 상속받아 사용하여야 했다.
// IoC 컨테이너가 관리하게 되면, 해당 Servlet을 상속 받을 일도 없고, 객
// com.eomcs.util.RequestMapping, RequestHandler, RequestMappingHandlerMapping 추가 //
// 원래는 기존에 삭제 되어있어야 했으나, 안지우고 계속 살려두고 와서 그 전 버전에도 계속 있던 것이다.
// ContextLoaderListener 변경 //
RequestMappingHandlerMapping handlerMapper = //
new RequestMappingHandlerMapping();
String[] beanNames = iocContainer.getBeanNamesForAnnotation(Component.class);
for (String beanName : beanNames) {
Object component = iocContainer.getBean(beanName);
Method method = getRequestHandler(component.getClass());
if (method != null) {
RequestHandler requestHandler = new RequestHandler(method, component);
handlerMapper.addHandler(requestHandler.getPath(), requestHandler);
}
}
servletContext.setAttribute("handlerMapper", handlerMapper);
// new RequestMappingHandlerMapping(); // 원래 handlerMapper.addHandler 위에 있는게 맞다.
// getBeanNamesForAnnotation(Component.class); // Component 애노테이션이 붙은 객체의 이름을 String 배열에 담는다.
// iocContainer.getBean(beanName); // 해당하는 이름을 가진 객체를 component에 담는다.
// getRequestHandler(component.getClass()); // 꺼낸 component의 정보에서 원하는 Method를 추출한다.
private Method getRequestHandler(Class<?> type) {
Method[] methods = type.getMethods();
for (Method m : methods) {
RequestMapping anno = m.getAnnotation(RequestMapping.class);
if (anno != null) {
return m;
}
}
return null;
}
// 원하는 Method를 추출하기 위해, 해당 메서드를 추가한다.
// 이 메서드의 목적은 component가 가지고 있는 메서드 중, RequestMapping이 붙은 1개의 메서드를 추출하는 역할이다.
// new RequestHandler(method, component); // method와 component를 Handler에 담는다.
// handlerMapper.addHandler(requestHandler.getPath(), requestHandler);
// handlerMapper에 해당 Handler의 경로(getPath())와 Handler를 담는다.
// 즉 위의 메서드는 RequestMapping이 붙은 메서드를 찾아서, RequestMappingHandlerMapping에 보관하는 것이다.
// 만든 RequestMappingHandlerMapping를 ServletContext에 담는다. // 다른데서도 사용하기 위함.
// DispatcherServlet 변경 //
@Override
public void init() throws ServletException {
handlerMapper = (RequestMappingHandlerMapping) getServletContext()//
.getAttribute("handlerMapper");
}
// getServletContext()에서 RequestMappingHandlerMapping를 꺼내서 저장한다.
// 자주 사용하기 때문에 변수에 미리 담아두는 것이다.
// 안담아두면 사용할 때마다 ServletContext을 호출하고 꺼내서 사용해야 함.
RequestHandler requestHandler = handlerMapper.getHandler(pathInfo);
String viewUrl = null;
if (requestHandler != null) {
try {
viewUrl = (String) requestHandler.getMethod().invoke(
requestHandler.getBean(),
request,
response
);
} catch (Exception e) {
StringWriter out = new StringWriter();
e.printStackTrace(new PrintWriter(out));
request.setAttribute("errorDetail", out.toString());
request.getRequestDispatcher("/error.jsp").forward(request, response);
return;
}
} else {
throw new Exception("해당 명령을 지원하지 않습니다.");
}
// handlerMapper에서 Client로부터 넘어온 pathInfo에 해당하는 Handler를 달라고 한다.
// handler가 들어있다면, requestHandler.getMethod().invoke로, 해당 메서드를 호출하고
// 해당 메서드의 객체와 해당 메서드에 넘겨줄 파라미터 request, response를 넘겨준다.
// request와 reponse는 당연히 HttpServlet을 의미한다. // 파라미터에 선언된 것이기 때문
// 해당하는 작업 실행중 오류가 발생하면, 오류를 받아 오류 jsp로 포워딩시킨다.
// viewUrl이 정상적으로 들어있다면, 해당 Url로 include한다.
// xxxServlet 변경 // Controller로 변경
@Component
public class BoardAddController {
@Autowired
BoardService boardService;
@RequestMapping("/board/add")
public String add(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (request.getMethod().equals("GET")) {
return "/board/form.jsp";
}
Board board = new Board();
board.setTitle(request.getParameter("title"));
boardService.add(board);
return "redirect:list";
}
}
// 모든 작업은 이제 jsp가 실행하므로, Servlet으로 존재할 이유가 없다. // 일반 클래스로 있어도 된다.
// 따라서 해당 작업에 해당하는 jsp을 리턴하게 변경한다. // 이게 위에서 viewUrl이다.
// com.eomcs.lms.filter.CharacterEncodingFilter 추가 // 요청 데이터에 한글 처리 필터를 붙힌다.
// 붙히지 않으면 Client가 작성한 한글을 정상적으로 불러올 수 없다.
v58_3 -> CRUD 페이지 컨트롤러를 한 클래스로 합친다.
@Component
public class BoardController {
@Autowired
BoardService boardService;
@RequestMapping("/board/add")
public String add(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (request.getMethod().equals("GET")) {
return "/board/form.jsp";
}
Board board = new Board();
board.setTitle(request.getParameter("title"));
boardService.add(board);
return "redirect:list";
}
@RequestMapping("/board/delete")
public String delete(HttpServletRequest request, HttpServletResponse response) throws Exception {
int no = Integer.parseInt(request.getParameter("no"));
if (boardService.delete(no) > 0) {
return "redirect:list";
} else {
throw new Exception("삭제할 게시물 번호가 유효하지 않습니다.");
}
}
@RequestMapping("/board/detail")
public String detail(HttpServletRequest request, HttpServletResponse response) throws Exception {
int no = Integer.parseInt(request.getParameter("no"));
Board board = boardService.get(no);
request.setAttribute("board", board);
return "/board/detail.jsp";
}
@RequestMapping("/board/list")
public String list(HttpServletRequest request, HttpServletResponse response) throws Exception {
List<Board> boards = boardService.list();
request.setAttribute("list", boards);
return "/board/list.jsp";
}
@RequestMapping("/board/update")
public String update(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (request.getMethod().equals("GET")) {
int no = Integer.parseInt(request.getParameter("no"));
Board board = boardService.get(no);
request.setAttribute("board", board);
return "/board/updateform.jsp";
}
Board board = new Board();
board.setNo(Integer.parseInt(request.getParameter("no")));
board.setTitle(request.getParameter("title"));
if (boardService.update(board) > 0) {
return "redirect:list";
} else {
throw new Exception("변경할 게시물 번호가 유효하지 않습니다.");
}
}
}
// ***Controller 변경 // 나뉘어 있던 기능을 게시판에 맞게 합친다.
// ContextLoaderListener 변경 //
String[] beanNames = iocContainer.getBeanNamesForAnnotation(Component.class);
for (String beanName : beanNames) {
Object component = iocContainer.getBean(beanName);
Iterator<Method> handlers = getRequestHandlers(component.getClass());
while (handlers.hasNext()) {
RequestHandler requestHandler = //
new RequestHandler(handlers.next(), component);
handlerMapper.addHandler(requestHandler.getPath(), requestHandler);
// 원래 RequestMapping 애노테이션이 붙은 Method가 한개씩있었던 Controller들을,
// 하나로 합쳐져서, Method가 여러개 존재하게 되었다.
// 따라서 Iterater<Method>를 리턴하게 하여, handler에 hasNext()로 다음 Method가 있는지 확인하고,
// 있다면, 다음 Method를 next()로 가져와서 저장하도록 한다.
private Iterator<Method> getRequestHandlers(Class<?> type) {
ArrayList<Method> handlers = new ArrayList<>();
Method[] methods = type.getMethods();
for (Method m : methods) {
RequestMapping anno = m.getAnnotation(RequestMapping.class);
if (anno != null) {
handlers.add(m);
}
}
return handlers.iterator();
}
// 파라미터로 넘어온 Class에 Method를 추출하여, 해당 메서드에 RequestMapping 애노테이션이 붙었는지 확인하고
// 붙은 메서드를 ArrayList에 담고, ArrayList를 리턴하지 않고, Iterator를 리턴한다.
v58_4 -> CRUD 페이지 컨트롤러를 한 클래스로 합친다.
public Map<String, Object> invoke(HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 페이지 컨트롤러가 작업한 결과를 받을 바구니를 준비한다.
HashMap<String, Object> model = new HashMap<>();
// 메서드의 파라미터 목록을 꺼낸다.
Parameter[] params = method.getParameters();
String viewUrl = (String) method.invoke(//
bean, // 메서드를 호출할 때 사용하는 인스턴스
getArguments(params, request, response, model) // 메서드에 넘겨 줄 값들
);
// request handler를 호출하면,
// model 객체에는 request handler가 담은 값이 보관되어 있다.
// 여기에 request handler의 리턴 값(JSP URL)도 함께 보관한다.
model.put("viewUrl", viewUrl);
// request handler의 작업 결과물과 JSP URL을 담은 맵 객체를 프론트 컨트롤러에게 리턴한다.
return model;
}
private List<String> getParameterNames(Class<?> type, Method method) throws Exception {
// => 컴파일할 때 옵션으로 파라미터 이름을 포함하지 않는 한에는
// 자바 reflection API로는 알아낼 수 없다.
// => 그러나 .class 파일에는 분명히 파라미터 이름을 들어 있다.
// => 그것을 꺼내려면 외부 라이브러리를 사용해야 한다.
//
Reflections reflections = new Reflections(type, // 클래스 타입
new MethodParameterNamesScanner() // 파라미터 이름 탐색 플러그인 장착
);
List<String> paramNames = reflections.getMethodParamNames(method);
logger.debug(String.format("%s.%s(", bean.getClass().getName(), method.getName()));
for (int i = 0; i < method.getParameterCount(); i++) {
logger.debug(String.format(" %s,", paramNames.get(i)));
}
logger.debug(")");
return paramNames.subList(0, method.getParameterCount());
}
private Object[] getArguments(Parameter[] params, HttpServletRequest request,
HttpServletResponse response, Map<String, Object> model) throws Exception {
// 파라미터 이름 알아내기
List<String> paramNames = getParameterNames(bean.getClass(), method);
// 파라미터 값을 담을 배열을 준비한다.
Object[] args = new Object[params.length];
// 각 파라미터에 대한 값을 준비한다.
for (int i = 0; i < params.length; i++) {
args[i] = getArgument(paramNames.get(i), params[i], request, response, model);
}
return args;
}
private Object getArgument(String paramName, Parameter param, HttpServletRequest request,
HttpServletResponse response, Map<String, Object> model) throws Exception {
Class<?> paramType = param.getType();
if (paramType == ServletRequest.class || paramType == HttpServletRequest.class) {
return request;
} else if (paramType == ServletResponse.class || paramType == HttpServletResponse.class) {
return response;
} else if (paramType == HttpSession.class) {
return request.getSession();
} else if (paramType == Map.class) {
return model;
} else if (paramType == Part.class) {
return request.getPart(paramName);
} else if (paramType == Part[].class) {
ArrayList<Part> values = new ArrayList<>();
Collection<Part> parts = request.getParts();
for (Part part : parts) {
if (part.getName().equals(paramName)) {
values.add(part);
}
}
return values.toArray(new Part[values.size()]);
} else if (isPrimitiveType(paramType)) {
return getPrimitiveValue(paramName, paramType, request);
} else {
return getPojoValue(paramName, paramType, request);
}
}
@SuppressWarnings("unchecked")
private Object getPojoValue(String paramName, Class<?> paramType, HttpServletRequest request)
throws Exception {
// 클라이언트가 보낸 데이터를 담을 POJO 객체를 생성한다.
// => 기본 생성자를 호출하여 인스턴스를 초기화시킨다.
Object pojo = paramType.getConstructor().newInstance();
// 세터 메서드를 추출한다.
Set<Method> methods = ReflectionUtils.getMethods(paramType, ReflectionUtils.withPrefix("set"));
logger.debug(String.format("%s 의 세터 메서드:", paramType.getName()));
// 클라이언트가 보낸 데이터 중에서 세터 메서드에 넘겨줄 데이터가 있다면 호출한다.
for (Method m : methods) {
// 메서드 이름에서 프로퍼티 명을 추출한다.
// => 예: setCreatedDate() => "c" + "reatedDate" = createdDate
String propName = m.getName().substring(3, 4).toLowerCase() + m.getName().substring(4);
logger.debug(" " + propName);
// 세터를 호출할 때 넘겨 줄 값을 담을 변수를 준비.
Object value = null;
if (isPrimitiveType(m.getParameters()[0].getType())) {
// 클라이언트가 보낸 데이터 중에서 프로퍼티 이름과 일치하는 데이터가 있다면 꺼낸다.
value = getPrimitiveValue(propName, m.getParameters()[0].getType(), request);
}
// 세터에 넘겨 줄 값을 준비하지 못했드면 다음 메서드로 넘어간다.
if (value == null) {
continue;
}
logger.debug(String.format(" %s()", m.getName()));
m.invoke(pojo, value);
}
return pojo;
}
private Object getPrimitiveValue(String paramName, Class<?> paramType,
HttpServletRequest request) {
// 자바 원시 타입일 경우 요청 파라미터에서 값을 찾는다.
String value = request.getParameter(paramName);
if (value == null) {
return null;
}
if (paramType == byte.class || paramType == Byte.class) {
try {
return Byte.parseByte(value);
} catch (Exception e) {
return (byte) 0;
}
} else if (paramType == short.class || paramType == Short.class) {
try {
return Short.parseShort(value);
} catch (Exception e) {
return (short) 0;
}
} else if (paramType == int.class || paramType == Integer.class) {
try {
return Integer.parseInt(value);
} catch (Exception e) {
return 0;
}
} else if (paramType == long.class || paramType == Long.class) {
try {
return Long.parseLong(value);
} catch (Exception e) {
return (long) 0;
}
} else if (paramType == float.class || paramType == Float.class) {
try {
return Float.parseFloat(value);
} catch (Exception e) {
return 0.0f;
}
} else if (paramType == double.class || paramType == Double.class) {
try {
return Double.parseDouble(value);
} catch (Exception e) {
return 0.0;
}
} else if (paramType == char.class || paramType == Character.class) {
try {
return value.charAt(0);
} catch (Exception e) {
return (char) 0;
}
} else if (paramType == boolean.class || paramType == Boolean.class) {
try {
return Boolean.parseBoolean(value);
} catch (Exception e) {
return false;
}
} else if (paramType == java.util.Date.class || paramType == java.sql.Date.class) {
try {
return java.sql.Date.valueOf(value); // 문자열 형식: "yyyy-MM-dd" 이어야 한다.
} catch (Exception e) {
return null;
}
}
return value;
}
private boolean isPrimitiveType(Class<?> paramType) {
if (paramType == byte.class || paramType == Byte.class || paramType == short.class
|| paramType == Short.class || paramType == int.class || paramType == Integer.class
|| paramType == long.class || paramType == Long.class || paramType == float.class
|| paramType == Float.class || paramType == double.class || paramType == Double.class
|| paramType == char.class || paramType == Character.class || paramType == boolean.class
|| paramType == Boolean.class || paramType == String.class
|| paramType == java.util.Date.class || paramType == java.sql.Date.class) {
return true;
}
return false;
}
// RequestHandler에 위 메서드들을 추가한다. // invoke 메서드를 만든다.
@Override
protected void service(//
HttpServletRequest request, //
HttpServletResponse response) throws ServletException, IOException {
try {
String pathInfo = request.getPathInfo();
response.setContentType("text/html;charset=UTF-8");
ArrayList<Cookie> cookies = new ArrayList<>();
request.setAttribute("cookies", cookies);
RequestHandler requestHandler = handlerMapper.getHandler(pathInfo);
String viewUrl = null;
if (requestHandler != null) {
try {
Map<String, Object> model = requestHandler.invoke(request, response);
viewUrl = (String) model.get("viewUrl");
// 요청 핸들러의 작업 결과를 꺼내서 JSP가 사용할 수 있도록
// ServletRequest 보관소에 저장한다.
Set<Entry<String, Object>> entrySet = model.entrySet();
for (Entry<String, Object> entry : entrySet) {
request.setAttribute(entry.getKey(), entry.getValue());
}
} catch (Exception e) {
StringWriter out = new StringWriter();
e.printStackTrace(new PrintWriter(out));
request.setAttribute("errorDetail", out.toString());
request.getRequestDispatcher("/error.jsp").forward(request, response);
return;
}
} else {
logger.info("해당 명령을 지원하지 않습니다.");
throw new Exception("해당 명령을 지원하지 않습니다.");
}
if (cookies.size() > 0) {
for (Cookie cookie : cookies) {
response.addCookie(cookie);
}
}
String refreshUrl = (String) request.getAttribute("refreshUrl");
if (refreshUrl != null) {
response.setHeader("Refresh", refreshUrl);
}
if (viewUrl.startsWith("redirect:")) {
response.sendRedirect(viewUrl.substring(9));
} else {
request.getRequestDispatcher(viewUrl).include(request, response);
}
} catch (Exception e) {
throw new ServletException(e);
}
}
}
// RequestHandler의 invoke를 호출하고 저장한 viewUrl과
// 해당 Controller의 method 결과를 담은 map 객체를 리턴한다.
@RequestMapping("/board/form")
public String form() throws Exception {
return "/board/form.jsp";
}
@RequestMapping("/board/add")
public String add(Board board) throws Exception {
boardService.add(board);
return "redirect:list";
}
// 프론트 컨트롤러(우리 프로젝트에서는 DispatcherServlet)의 변경에 맞춰 페이지 컨트롤러(xxxController)를 변경한다.
v59_1 -> Spring WebMVC 적용
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'spring-webmvc' 검색
// org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// lms.ContextLoaderListener, filter.CharacterEncodingFilter, servlet.DispatcherServlet 삭제
// util.RequestMapping, RequestHandler, RequestMappingHandlerMapping 삭제
// 기존에 임시로 만든 Spring class는 삭제한다.
<filter>
<filter-name>CharacterEncodingFilter</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>CharacterEncodingFilter</filter-name>
<url-pattern>/app/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- DispatcherSerlvet이 사용할 IoC 컨테이너를 지정한다. -->
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.eomcs.lms.AppConfig</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<multipart-config>
<max-file-size>5000000</max-file-size>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
// src/main/webapp/WEB-INF/web.xml 변경 //
// springframework 안에 DispatcherServlet 클래스, CharacterEncodingFilter 클래스를 등록한다. //
// 또한 파일첨부 , 사진 첨부 등을 사용하기 위한 multipart-config를 등록한다.
@Controller
public class BoardController {
static Logger logger = LogManager.getLogger(BoardController.class);
@Autowired
BoardService boardService;
public BoardController() {
logger.debug("BoardController 생성됨!");
}
@RequestMapping("/board/form")
public String form() throws Exception {
return "/board/form.jsp";
}
@RequestMapping("/board/add")
public String add(Board board) throws Exception {
boardService.add(board);
return "redirect:list";
}
// xxxController 변경 //
// @Controller 애노테이션을 붙힌다. // springframework에서는 Controller임을 알려주기 위한 애노테이션을 설정해야 한다.
v59_2 -> Spring WebMVC 적용
public class AppWebApplicationInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {AppConfig.class};
}
@Override
protected Filter[] getServletFilters() {
return new Filter[] { //
new CharacterEncodingFilter("UTF-8") //
};
}
@Override
protected String[] getServletMappings() {
return new String[] {"/app/*"};
}
@Override
protected String getServletName() {
return "app1";
}
@Override
protected void customizeRegistration( //
javax.servlet.ServletRegistration.Dynamic registration) {
registration.setMultipartConfig(//
new MultipartConfigElement(uploadTmpDir, // 업로드 한 파일을 임시 보관할 위치
10000000, // 최대 업로드할 수 있는 파일들의 총 크기
15000000, // 요청 전체 데이터의 크기
2000000 // 업로드 되고 있는 파일을 메모리에 임시 임시 보관하는 크기
));
}
}
// web.AppWebApplicationInitializer 추가
// customizeRegistration() //
// Servlet 3.0 API의 파일 업로드를 사용하려면 DispatcherServlet에 설정을 추가해야 한다.
// 즉 멀티파트 데이터를 Part 객체로 받을 때는 설정을 추가해야 한다.
// DispatcherServlet 이 multipart/form-data 으로 전송된 데이터를 처리하려면 해당 콤포넌트를 등록해야 한다.
// 단 이 설정을 추가하면 Spring WebMVC의 MultipartResolver가 작동되지 않는다.
@ComponentScan(value = "com.eomcs.lms")
@EnableWebMvc
public class AppConfig {
static Logger logger = LogManager.getLogger(AppConfig.class);
public AppConfig() {
logger.debug("AppConfig 객체 생성!");
}
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver vr = new InternalResourceViewResolver(//
"/WEB-INF/jsp/", // prefix
".jsp" // suffix
);
return vr;
}
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver mr = new CommonsMultipartResolver();
mr.setMaxUploadSize(10000000);
mr.setMaxInMemorySize(2000000);
mr.setMaxUploadSizePerFile(5000000);
return mr;
}
// AppConfig 변경 //
// multipartResolver() //
// Spring의 방식으로 파일 업로드를 처리하고 싶다면, AppConfig.java에 MultipartResolver를 추가해야 한다.
// @EnableWebMvc //
// SpringMvc를 구성할때 필요한 Bean설정들을 자동으로 해주는 애노테이션
// WebMVC 관련 애노테이션을 처리할 객체 등록
// .jsp 파일 이동 // JSP 파일을 /WEB-INF/jsp/ 폴더로 옮긴다.
@Controller
@RequestMapping("/board")
public class BoardController {
static Logger logger = LogManager.getLogger(BoardController.class);
@Autowired
BoardService boardService;
public BoardController() {
logger.debug("BoardController 생성됨!");
}
@GetMapping("form")
public void form() throws Exception {}
@PostMapping("add")
public String add(Board board) throws Exception {
boardService.add(board);
return "redirect:list";
}
// xxxController 변경 //
// @RequestMapping("경로") 설정 // 클래스에 선언할 수도, 메서드에 선언할 수도 있다.
// @GetMapping("메서드명"), @PostMapping // 클래스에 @RequestMapping을 설정한 경우,
// Get과 Post 요청을 구분하여 해당 메서드를 호출 할 수 있게 애노테이션을 부여한다.
@ControllerAdvice
public class GlobalControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) {
// String 날짜 ==> java.sql.Date 객체
binder.registerCustomEditor( //
java.util.Date.class, //
new PropertyEditorSupport() { //
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(java.sql.Date.valueOf(text));
}
});
}
}
// GlobalControllerAdvice 추가 //
// 페이지 컨트롤러에 보조할 객체를 등록하기 위해 @ControllerAdvice 클래스를 정의한다.
// 날짜 파라미터를 처리하기 위해 @InitBinder 메서드를 정의한다.
v59_3 -> DispatcherServlet 여러 개 설정하기
// Spring의 WebApplicationInitializer 구현체를 통해 여러 개의 DispatcherServlet을 설정할 수 있다.
// 공통 컴포넌트와 DispatcherServlet 전용 컴포넌트를 분리하여 관리할 수 있다.
@ComponentScan( //
value = "com.eomcs.lms", //
excludeFilters = {//
@Filter(//
type = FilterType.REGEX, //
pattern = "com.eomcs.lms.web.*"), //
@Filter(//
type = FilterType.REGEX, //
pattern = "com.eomcs.lms.admin.*")//
})
public class AppConfig {
static Logger logger = LogManager.getLogger(AppConfig.class);
public AppConfig() {
logger.debug("AppConfig 객체 생성!");
}
// FilterType.REGEX // 해당 pattern에 일치하는 부분을 @Filter한다고 생각하면 된다.
// 단 excludeFilters이기 때문에, web 및, admin 및의 폴더는 제외된,
// 나머지에서 component 애노테이션을 붙은 class를 적용한다 생각하면 된다.
@ComponentScan(value = "com.eomcs.lms.web")
@EnableWebMvc
public class AppWebConfig {
static Logger logger = LogManager.getLogger(AppWebConfig.class);
public AppWebConfig() {
logger.debug("AppWebConfig 객체 생성!");
}
@ComponentScan(value = "com.eomcs.lms.admin")
@EnableWebMvc
public class AdminWebConfig {
static Logger logger = LogManager.getLogger(AdminWebConfig.class);
public AdminWebConfig() {
logger.debug("AdminWebConfig 객체 생성!");
}
// AppWebConfig, AdminWebConfig 생성 //
// AppConfig에서 제외한 web 및 폴더와, admin 및 폴더에서 Component를 찾을 AppConfig 파일을 생성한다.
public class AppWebApplicationInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] {AppConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {AppWebConfig.class};
}
@Override
protected Filter[] getServletFilters() {
return new Filter[] { //
new CharacterEncodingFilter("UTF-8") //
};
}
@Override
protected String[] getServletMappings() {
return new String[] {"/app/*"};
}
@Override
protected String getServletName() {
return "app";
}
}
public class AdminWebApplicationInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {AdminWebConfig.class};
}
@Override
protected Filter[] getServletFilters() {
return new Filter[] { //
new CharacterEncodingFilter("UTF-8") //
};
}
@Override
protected String[] getServletMappings() {
return new String[] {"/admin/*"};
}
@Override
protected String getServletName() {
return "admin";
}
}
// admin/AdminWebApplicationInitializer.java 추가, web/AppWebApplicationInitializer.java 변경 //
v60_1 -> 뷰 컴포넌트에 Tiles 기술 적용하기
// Tiles를 JSP와 결합하여 뷰 컴포넌트를 구성할 수 있다.
// Tiles 템플릿의 레이아웃을 구성할 수 있다.
// mvnmvnrepository.com 또는 search.maven.org 접속 - 'tiles-jsp' 검색
// org.springframework:spring-context // 버전 클릭 // Gradle Groovy DSL 복사
// - 라이브러리 정보를 dependencies {} 블록에 추가 - 'gradle cleanEclipse' - 'gradle eclipse'
// web.AppWebConfig 변경 //
@ComponentScan(value = "com.eomcs.lms.web")
@EnableWebMvc
public class AppWebConfig {
static Logger logger = LogManager.getLogger(AppWebConfig.class);
public AppWebConfig() {
logger.debug("AppWebConfig 객체 생성!");
}
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver vr = new InternalResourceViewResolver(//
"/WEB-INF/jsp/", // prefix
".jsp" // suffix
);
vr.setOrder(2);
return vr;
}
@Bean
public ViewResolver tilesViewResolver() {
UrlBasedViewResolver vr = new UrlBasedViewResolver();
// Tiles 설정에 때라 템플릿을 실행할 뷰 처리기를 등록한다.
vr.setViewClass(TilesView.class);
// 뷰리졸버의 우선 순위를 InternalResourceViewResolver 보다 우선하게 한다.
vr.setOrder(1);
return vr;
}
@Bean
public TilesConfigurer tilesConfigurer() {
TilesConfigurer configurer = new TilesConfigurer();
configurer.setDefinitions("/WEB-INF/defs/tiles.xml");
return configurer;
}
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver mr = new CommonsMultipartResolver();
mr.setMaxUploadSize(10000000);
mr.setMaxInMemorySize(2000000);
mr.setMaxUploadSizePerFile(5000000);
return mr;
}
// 원래는 ViewResolver를 두가지를 사용하지는 않지만, setOrder를 설정하여 어떤 View를 선택하게 할 수 있다.
// ViewResolver()를 vr.setOrder(1); // jsp 파일 경로가 실행된다.
// TilesView // TilesView 템플릿 엔진을 추가한다.
// TilesConfigurer // Tiles의 템플릿을 설정한다.
// setDefinitions("경로") // Tiles의 템플릿을 정의해둔 파일을 알려준다.
<?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>
<!-- 여러 템플릿에서 공통으로 사용할 레이아웃을 정의한다. -->
<definition name="base" template="/WEB-INF/tiles/template.jsp">
<!-- template.jsp 안에서 사용할 JSP 파일의 이름을 설정한다. -->
<put-attribute name="header" value="/WEB-INF/tiles/header.jsp" />
<put-attribute name="footer" value="/WEB-INF/tiles/footer.jsp" />
</definition>
<!-- request handler가 리턴한 JSP의 경로가 'board/*' 일 경우
TilesView 템플릿 엔진이 사용할 레이아웃을 정의한다. -->
<definition name="*/*" extends="base">
<put-attribute name="body" value="/WEB-INF/views/{1}/{2}.jsp" />
</definition>
</tiles-definitions>
// WEB-INF/defs/tiles.xml 생성 // 템플릿의 레이아웃을 정의한다.
// name은 기본적으로 request handler가 리턴한 값을 의미한다.
// 그러나 request handler가 리턴할 수 없는 값 // 보통 base, definition, default 등등 자유롭게 준다.
// name으로 설정하게 되면, 이 파일은 tiles 적용을 안받는 파일이라는 것을 의미한다.
// 주로, tiles 적용은 안받지만, 다른 파일에 적용할 수 있게 header, footer, left 등을 주로 넣는다.
// 그래야 template 틀(template.jsp)에서 사용이 가능하기 때문이다. //
// template="파일" // 다른 파일에 이 틀을 적용한다고 알려주는 것이다. // 바로 아래에 나옴
// name="*/*" // 이 프로젝트에서는 request handler가 리턴하는 값이 board/list, lesson/list 등 이렇게 되기 때문에,
// *(와일드 카드) 순서에 따라 {1} , {2} 이런식으로 구분을 한다.
// 즉, board/list 처럼 */* 이런 형식을 리턴하면, /WEB-INF/views/board/list.jsp를 "body"라는 이름으로 저장한다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"
trimDirectiveWhitespaces="true"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<c:if test="${not empty refreshUrl}">
<meta http-equiv="Refresh" content="${refreshUrl}">
</c:if>
<title>Bitcamp-LMS</title>
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css' integrity='sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh' crossorigin='anonymous'>
<style>
body {
background-color: LightGray;
}
div.container {
background: white;
border: 1px solid gray;
width: 600px;
}
footer {
margin-top: 20px;
padding: 10px;
background-color: navy;
color: white;
text-align: center;
}
</style>
</head>
<body>
<tiles:insertAttribute name="header"/>
<div class='container'>
<tiles:insertAttribute name="body"/>
</div>
<tiles:insertAttribute name="footer"/>
</body>
</html>
// /webapp/WEB-INF/tiles/template.jsp 생성 //
// tiles.xml에서 저장한 header, body, footer가 들어갈 위치를 설정한다.
// 단, css의 경우 head 태그 안에만 선언이 되어야 하기 때문에 css 적용하는데에는 여러 방법이 있다.
// 1. 적용할 css를 한 파일에 선언하고, template.jsp 파일에 link하는 방식
// 2. 만약 css를 사용하는 데마다 별도로 지정해야 한다면,
// template.jsp에 request handler의 리턴값을 통해 css파일의 위치를 선언하여, 넣는 방법이 있다.
// 그 외에도 여러가지 방법이 있다. // 그러나 body안에 css 태그를 넣을 수는 없다.
// 또한 WEB-INF안에 jsp 파일 외에 다른 파일을 두면 인식을 하지 못하기 때문에 CSS파일과 JS파일을 별도로 둔다면,
// WEB-INF 밖에 webapp 밑에 별도로 둬야 한다. // 당연히 경로를 지정해줘야 한다. // 경로는 절대경로도 되고, 상대경로도 된다.
<definition name="*/*" extends="base">
<put-attribute name="body" value="/WEB-INF/views/{1}/{2}.jsp" />
<put-attribute name="css.page" value="../../css/{1}.css" />
<put-attribute name="css.common" value="../../css/common.css" />
</definition>
// tiles.xml 파일에 이렇게 css를 설정한다. // common.css는 모든 페이지가 공통으로 적용 받을 값이기 때문에 고정 값으로 넣는다.
// css.page는 page별로 다르게 css를 적용해야 하는 경우에 저런 형식으로 불러온다는 의미이다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"
trimDirectiveWhitespaces="true"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<c:if test="${not empty refreshUrl}">
<meta http-equiv="Refresh" content="${refreshUrl}">
</c:if>
<title>Bitcamp-LMS</title>
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css' integrity='sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh' crossorigin='anonymous'>
<link rel='stylesheet' href='<tiles:getAsString name="css.common"/>'>
<link rel='stylesheet' href='<tiles:getAsString name="css.page"/>'>
</head>
<body>
<tiles:insertAttribute name="header"/>
<div class='container'>
<tiles:insertAttribute name="body"/>
</div>
<tiles:insertAttribute name="footer"/>
</body>
</html>
// template.jsp //
// 공통적으로 적용되는 css.common을 css.page보다 위에 두어야 한다. // 그래야 css.page가 나중에 덮어쓸 수 있기 때문이다.
// tilles를 사용하려면 /webapp/WEB-INF/jsp 폴더를 복사하여 /WEB-INF/views 이름에 옮긴다. // 경로를 그렇게 설정했기 때문
// 템플릿 사용에 맞춰서 JSP 파일을 편집한다. // include 코드를 삭제한다. // tiles에서 넣어주기 때문
$ scoop install nodejs // scoop으로 설치 후 gitbash를 껏다가 켜야 한다.
// $ npm -v // 설치 여부를 확인할 수 있다. 버전이 뜨면 다운로드가 된 것이다.
// $ npm init // src/main/webapp에 들어가서 커맨드 입력해야 한다. // 다 엔터치면 된다.
// init가 끝났다면 webapp안에 package.json이 생긴다.
// $ npm install bootstrap --save //
"dependencies": {
"bootstrap": "^4.5.0"
}
// package json에 이렇게 bootstrap이 추가 된 것을 알 수 있다.
<link rel='stylesheet' href='${pageContext.getServletContext().getContextPath()}/node_modules/bootstrap/dist/css/bootstrap.min.css'>
<link rel='stylesheet' href='<tiles:getAsString name="css.common"/>'>
<link rel='stylesheet' href='<tiles:getAsString name="css.page"/>'>
// 이렇게 경로를 지정해 준다.
<script src='${pageContext.getServletContext().getContextPath()}/node_modules/jquery/dist/jquery.min.js'></script>
<script src='${pageContext.getServletContext().getContextPath()}/node_modules//@popperjs/core/dist/umd/popper.min.js'></script>
<script src='${pageContext.getServletContext().getContextPath()}/node_modules/bootstrap/dist/js/bootstrap.min.js'></script>
<script src='${pageContext.getServletContext().getContextPath()}/node_modules/swweetalert/dist/sweetalert.
// 만약 git에 올리게 되면 package.json만 올라가게 된다. // 다른 팀원이 받은 외부 라이브러리를 사용하려면,
// webapp 경로에까지 들어가서 $ npm install 입력하면 package.json에 정의 된 대로 다운이 받아진다.
'IT Developer > Bitcamp' 카테고리의 다른 글
비트캠프 프론트엔드 및 벡엔드 개발자 Webapp css (0) | 2020.04.27 |
---|---|
비트캠프 프론트엔드 및 백엔드 개발자 Spring-webmvc, java-web-library (0) | 2020.04.17 |
비트캠프 프론트엔드 및 벡엔드 개발자 Webapp el, jsp, jstl (0) | 2020.04.13 |
비트캠프 프론트엔드 및 벡엔드 개발자 Web (0) | 2020.04.03 |
비트캠프 프론트엔드 및 벡엔드 개발자 DB (0) | 2020.03.26 |
비트캠프 프론트엔드 및 벡엔드 개발자 net, netty, reflect (0) | 2020.03.11 |
비트캠프 프론트엔드 및 벡엔드 개발자 ioc, jdbc, mybatis, (0) | 2020.03.11 |
비트캠프 프론트엔드 및 백엔드 개발자 #Project v43 ~ v54 (0) | 2020.03.10 |