WEB 서버를 사용하는 경우 정적 파일 이나, Default 요청에 대한 자원 관리는 WAS 가 처리하지 않고, WEB 서버에게 위임 한다.


WAS(Web Application Server) 만 사용하는 경우 Servlet 등록 하고, 프로그래밍 하여 동적 페이지를 생성 한다. 모든 요청을 받는 Servlet 을 작성 하는 경우 별도 처리를 해줘야 한다.


Spring MVC 를 사용한다고 하면, 대표적으로 DispatcherServlet 를 등록하고, 모든 요청을 받게 설정을 한다.


web.xml


<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:dispatcher-servlet.xml
        </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>


위와 같이 하는 경우 정적 파일 까지 포함해서 DispatcherServlet 이 요청을 받게 된다. 해당 요청을 처리하기 위해서는 별도의 설정이 필요 하다.


1. mvc:view-controller


dispatcher-servlet.xml


<mvc:view-controller path="/" view-name="index"/>


위 소스를 추가 하자. DispatcherServlet 에 view-controller 설정을 했다는 것은 해당 path 의 요청이 오면 view-name 으로 페이지를 연결 시켜 준다. 해당 기능은 Spring 의 AbstractController 가 AbstractUrlViewController 에게 위임 하고 있다. 아래 코드는 실제 구현 되어있는 부분이다.


protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) {
    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    String viewName = getViewNameForRequest(request);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Returning view name '" + viewName
                + "' for lookup path [" + lookupPath + "]");
    }
    return new ModelAndView(viewName,
            RequestContextUtils.getInputFlashMap(request));
}


view-controller 기능을 사용하면 직접 구현하지 않아도 편하게 개발이 가능 하다.


2. mvc:resources


dispatcher-servlet


<mvc:resources location="/css/**" mapping="/css/**"/>
<mvc:resources location="/image/**" mapping="/image/**"/>
<mvc:resources location="/js/**" mapping="/js/**"/>


위 소스를 추가 하자. 설명은 Tomcat 으로 한다. location 에 대한 요청이 들어오면 해당 요청을 Tomcat 의 DefaultServlet 에 위임 한다. DefaultServlet 는 tomcat/conf/web.xml 을 참고 하면 된다.


web.xml


<servlet>
    <servlet-name>default</servlet-name>
    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
    <init-param>
        <param-name>debug</param-name>
        <param-value>0</param-value>
    </init-param>
    <init-param>
        <param-name>listings</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<!-- The mapping for the default servlet -->
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>


기본적으로 등록 되어 있는 Servlet 이다. webapps 경로를 찾아 mapping 에 대한 파일을 응답 한다.


설명한 두가지 방법을 활용 한다면, WAS 만 사용하는 프로젝트나, 아니면 임시로 설정하여 개발하는대 많은 도움이 될 것이다.


소개


Spring MVC 사용 시 DispatcherServlet 기능을 사용 한다. requestUri 에 따라 Controller 로 분기를 하고, 비지니스 로직 처리 후 Resolver 를 사용하여 해당 JSP 파일을 찾아 응답 하게 되는대 그 사이의 시점을 잡아 처리 하는 부분이 AbstractView 의 기능이다.


범용적으로 사용하는 Resolver 는 InternalResourceViewResolver 이다. 우리는 그 전에 DownloadView 를 구현하여 파일을 다운로드 할 것이다.


적용 방법

1. DownloadView


public class DownloadView extends AbstractView {

    public DownloadView() {
        setContentType("applicaiton/download;charset=utf-8");
    }

    private void setDownloadFileName(String fileName, HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
        String userAgent = request.getHeader("User-Agent");

        boolean isIe = userAgent.indexOf("MSIE") != -1;

        if(isIe){
            fileName = URLEncoder.encode(fileName, "utf-8");
        } else {
            fileName = new String(fileName.getBytes("utf-8"));
        }

        response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\";");
        response.setHeader("Content-Transfer-Encoding", "binary");
    }

    private void downloadFile(File downloadFile, HttpServletRequest request, HttpServletResponse response) throws Exception {
        OutputStream out = response.getOutputStream();
        FileInputStream in = new FileInputStream(downloadFile);

        try {
            FileCopyUtils.copy(in, out);
            out.flush();
        } catch (Exception e) {
            throw e;
        } finally {
            try { if (in != null) in.close(); } catch (IOException ioe) {}
            try { if (out != null) out.close(); } catch (IOException ioe) {}
        }
    }

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

            File downloadFile = (File) model.get("downloadFile");

            if (logger.isDebugEnabled()) {
                logger.debug("downloadFile: " + downloadFile);
            }

            this.setDownloadFileName(downloadFile.getName(), request, response);

            response.setContentLength((int) downloadFile.length());
            this.downloadFile(downloadFile, request, response);
        } catch (Exception e) {
            throw e;
        }
    }
}


2. applicationContext.xml


파일 하단에 추가 하자.


<beans:bean id="downloadView" class="kr.co.whitelife.DownloadView" />


3. SampleController


View 를 DownloadView 로 교체 한 후 파일 객체를 넘겨 주자.


@Controller
public class SampleController {

    @Resource(name="downloadView")
    private View downloadView;

    @RequestMapping(value="/sample", method=RequestMethod.GET})
    public ModelAndView sample() {
        ModelAndView mav = new ModelAndView();
        mav.setView(this.downloadView);

        File downloadFile = new File("downloadFile");
        mav.addObject("downloadFile", downloadFile);

        return mav;
    }
}


4. SampleRequest


파일을 첨부 하여 http://localhost:8080/sample 요청 해 보자.


Spring MVC 사용 시 SimpleMappingExceptionResolver 설정을 해놓았다. Controller 에서 공통으로 필요한 부분을 AOP 로 만들어서 사용 하는대, Exception 발생 시 AOP Proxy 에서 예외가 발생 했다. 정작 필요한 에러 메시지는 볼 수 없었다.


일반적으로 AOP 설정을 하는 경우 아래와 같이 선언을 했었다.


<aop:config>    
    // Doing...
</aop:config>


<aop:config> 에 아무런 설정 값을 주지 않은 경우 기본적으로 JDK Proxy 로 동작 한다. Spring 에서 제공 하는 InvocableHandlerMethod.invoke(Object[] arg) 메소드를 사용 하여 실제 targetClass를 호출 한다. Java Reflection 기반 이라고 생각 하면 된다.


에러 메시지의 일부 이다. invoke 호출 시 예외가 발생 한다. targetClass 가 Interface 를 구현하고 있는 경우 instance 를 인식하지 못해 예외가 발생 하는 것 같다.


java.lang.IllegalStateException: The mapped controller method class 'com.xxx.xxx' is not an instance of the actual controller bean instance '$Proxy22'. If the controller requires proxying (e.g. due to @Transactional), please use class-based proxying.
HandlerMethod details: 
Controller [$Proxy22]
Method [public org.springframework.web.servlet.ModelAndView com.xxx.xxx.xxxxxx() throws java.lang.Exception]
// 생략...


문제 해결을 위해서는 Class 기반 proxy 사용을 해야 한다.
please use class-based proxying. 이 메시지가 친절하게 방법을 알려 준다.


CGLIB Proxy 를 사용하려면 아래와 같이 설정 값을 변경 한다.


<aop:config proxy-target-class="true">
    // Doing...
</aop:config>


테스트를 해보자. 정상적으로 동작 할 것 이다.


참고 사이트


소개


Spring 3.0 이상 부터는 Ajax 처리 시 아래와 같은 방법으로 쉽게 처리가 가능 하다.


RequestMapping(value="/sample", method=RequestMethod.GET})
public @ResponseBody String sample() {
    return "sample";
}


하지만 범용적으로 사용하고 싶을 때가 있을 것 이다. 그런 경우 사용하는 방법 이다. 아래 설명을 참고하여 적용 해 보자.


적용 방법

1. AjaxView


public class AjaxView extends AbstractView {

    public AjaxView() {
        setContentType("text/json;charset=utf-8");
    }

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

            String reponseString = "";

            // responseString Doing...

            PrintWriter writer = response.getWriter();
            writer.print(reponseString);
            writer.flush();

            try { if (writer != null) { writer.close(); } } catch (IOException ioe) {}
        } catch (Exception e) {
            throw e;
        }
    }
}


2. applicationContext.xml


파일 하단에 추가 하자.


<beans:bean id="ajaxView" class="kr.co.whitelife.AjaxView" />


3. SampleController


View 를 ajaxView 로 교체 하자.


@Controller
public class SampleController {
    @Resource(name="ajaxView")
    private View ajaxView;

    @RequestMapping(value="/sample", method=RequestMethod.GET})
    public ModelAndView sample() {
        ModelAndView mav = new ModelAndView();
        mav.setView(this.ajaxJsonView);

        // Doing...

        return mav;
    }
}


4. Sample Request


http://localhost:8080/sample 요청 해 보자.


설명


Spring MVC 에서 Excel 기능을 제공하는 View 가 있다.


  • AbstractExcelView (라이브러리에서 제공 하는 소스 일부 이다.)
@Component("AbstractExcelView")
public abstract class AbstractExcelView extends AbstractView {

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

    /**
     * Subclasses must implement this method to create an Excel HSSFWorkbook document,
     * given the model.
     * @param model the model Map
     * @param workbook the Excel workbook to complete
     * @param request in case we need locale etc. Shouldn't look at attributes.
     * @param response in case we need to set cookies. Shouldn't write to it.
     */
    protected abstract void buildExcelDocument(
            Map<String, Object> model, HSSFWorkbook workbook, HttpServletRequest request, HttpServletResponse response)
            throws Exception;
}


AbstractView 기능을 구현하기 위해서는 기본적으로 renderMergedOutputModel 메소드를 구현해야 한다. 시점은 Controller 처리 후 라고 보면 쉽게 이해 할 수 있다. model 값은 Controller 에서 modelMap, ModelAndView 에 넣었던 데이터들 이라고 생각 하면 된다.


AbstractExcelView 의 경우에는 Excel 생성 부분 buildExcelDocument 메소드만 Override 하여 구현 하면 됬는대, 인자로 넘어오는 HSSFWorkbook 는 Excel2003 버전을 지원 한다. 확장자 (.xls) 그 이상 버전을 지원하는 poi 라이브러리를 사용 하는 경우 AbstractView 를 직접 구현해야 한다.


적용 방법

1. Dependency Library 추가


<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.10.1</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>3.10.1</version>
</dependency>


2. SampleController 구현 하기


  • SampleController
@Controller("SampleController")
public class SampleController {

    @RequestMapping(value="/sample", method=RequestMethod.POST)
    public ModelAndView sample () {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("SampleExcelView");

        return mav;
    }

}


3. SampleExcelView 구현 하기


  • SampleExcelView
@Component("SampleExcelView")
public class SampleExcelView extends AbstractView {

    /** The content type for an Excel response */
    private static final String CONTENT_TYPE = "application/vnd.ms-excel";

    public AdminExcelView() {
        setContentType(CONTENT_TYPE);
    }

    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            Workbook workbook = new XSSFWorkbook();
            XSSFSheet sheet = (XSSFSheet) workbook.createSheet();

            // Doing

            ServletOutputStream out = response.getOutputStream();
            workbook.write(out);

            if (out != null) out.close();
        } catch (Exception e) {
            throw e;
        }
    }
}


Doing 부분을 AbstractExcelView 의 buildExcelDocument 구현하는 것 처럼 작성 하면 Excel2007 이상 지원하는 (.xlsx) 파일을 작성 할 수 있다.


참고 사이트


1. Dependency Library 추가


Java Poi 라이브러리를 사용 Maven pom.xml 에 추가 하자.


<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.0.2-FINAL</version>
</dependency>


2. AbstractExcelView 구현 하기


Spring 에서 제공 하는 AbstractView 기능을 확장하여 사용 한다.

TestExcelView.java
@Component("TestExcelView")
public class TestExcelView extends AbstractExcelView {

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

        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        HSSFSheet sheet = workbook.createSheet();
        HSSFRow header = sheet.createRow(0);
        header.createCell((short) 0).setCellValue("Test1");
        header.createCell((short) 1).setCellValue("Test2");
        header.createCell((short) 2).setCellValue("Test3");
        header.createCell((short) 3).setCellValue("Test4");
        header.createCell((short) 4).setCellValue("Test5");

        HSSFRow row = sheet.createRow(1);
        row.createCell((short) 0).setCellValue("Test1");
        row.createCell((short) 1).setCellValue("Test2");
        row.createCell((short) 2).setCellValue("Test3");
        row.createCell((short) 3).setCellValue("Test4");
        row.createCell((short) 4).setCellValue("Test5");
    }

}


3. Controller 구현 하기


ModelAndView Return 시 앞서 구현한 TestExcelView 로 변경 하자.

TestController.java
@RequestMapping(value="/testExcel.xls", method=RequestMethod.GET)
public ModelAndView testExcel(HttpServletRequest request) {
    ModelAndView mav = new ModelAndView();

    mav.setViewName("TestExcelView");

    return mav;
}


4. 요청 하기


http://localhost:8080/testExcel.xls

참고 서적
  • Spring Recipes - A Problem-Solution Approach, 2nd Edition (Apress, 2010) (364~365)


설명


Controller 에 @InitBinder 기능을 추가로 사용 한다. WebDataBinder.class 를 인자로 받게 된다.

Sample


binder.registerCustomEditor(requiredType, field, propertyEditor);
  • requireType: 파라미터 Class
  • field: 파라미터 명
  • propertyEditor: 파라미터 처리 핸들러

해당 코드는 Integer[] 타입의 tests=123&tests=124&tests=125 요청을 처리 하는 핸들러 이다. 필요에 따라 setAsText 함수를 Override 하여 값 수정 후 setValue(text) 처리를 하자.


@InitBinder
protected void initBinder(WebDataBinder binder) {

    binder.registerCustomEditor(Integer[].class, "tests", new PropertyEditorSupport() {

        public void setAsText(String text) throws IllegalArgumentException {
            // text: field value

            if (text.indexOf("&") == -1) {
                setValue(Integer.valueOf(text.split("=")[1]));
                return;
            }

            List<Integer> tests = new ArrayList<Integer>();

            for (String test : text.split("&")) {
                tests.add(Integer.valueOf(test.split("=")[1]));
            }

            setValue(tests.toArray());
        }
    });
}


확인 하기


@RequestMapping(value="/request", method=RequestMethod.POST)
public @ResponseBody String request(Test test) throws Exception {
    return Arrays.toString(test.getTests());
}

참고 사이트


Gson Sample


Gson 기능 중 Object, List 를 Json 형태로 변환해주는 기능이 있다.


public String getJson (List sampleList) {
    return gson.toJson(sampleList, ArrayList.class);
}


위와 같은 코드를 작성 하면 쉽게 처리가 가능 하다.


Spring MVC Sample


@RequestMapping(value="/sampleJson", method=RequestMethod.GET, produces="text/plain;charset=UTF-8")
public @ResponseBody String sampleJson(HttpServletRequest request, HttpServletResponse response) throws Exception {
    try {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("sample.list");

        return this.gson.toJson(sampleService.selectList(), ArrayList.class);
    } catch (Exception e) {
        throw e;
    }
}


위 소스를 참고 하면 Json String 으로 변환 하여 응답 한다. Encoding 문제로 한글이 물음표로 께지는 현상이 있다.


produces="text/plain;charset=UTF-8"


@RequestMapping 에 해당 값을 추가 하자. 한글이 정상적으로 출력 될 것이다.


+ Recent posts