지금까지 Spring 설정파일이나 Web.xml 파일의 JavaConfig형태로 사용하는것에 대해서 큰 필요성을 느끼지 못해 굳이 사용하고 있지 않았었다.


그런데 최근 Filter를 운영환경에서만 추가로 설정해야 할 필요가 생겨났다.

Filter를 상속받아서 처리해도 되겠지만 생각난김에 web.xml을 Java Code로 변경해 보았다.


[WebXml.java]

package config;

import java.util.EnumSet;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.multipart.support.MultipartFilter;
import org.springframework.web.servlet.DispatcherServlet;

import pe.kr.ddakker.core.framework.web.GlobalsProperties;
import pe.kr.ddakker.core.support.util.StringUtils;

/**
 * /WEB-INF/Web.xml 설정
 * @author ddakker 2014. 10. 13.
 */
public class WebXml implements WebApplicationInitializer {
	private static Logger log = LoggerFactory.getLogger(WebXml.class);

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		// 스프링 설정
		XmlWebApplicationContext rootContext = new XmlWebApplicationContext();
        rootContext.setConfigLocations(new String[] { "classpath:config/spring/context-*.xml" });
        rootContext.refresh();
        rootContext.start();


        GlobalsProperties globalsProperties = rootContext.getBean("globalsProperties", GlobalsProperties.class);
        String serverType 	= StringUtils.defaultString(globalsProperties.getProperty("server.type"), "local")
        String domain		= GlobalsProperties.REAL.equals(serverType)?"board.ddakker.pe.kr":serverType + "-board.ddakker.pe.kr";
        log.debug("========== domain: {} ========== ", domain);
        log.debug("========== serverType: {} ========== ", serverType);


        // SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS Filter 영역 SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS
        if (GlobalsProperties.STAGE.equals(serverType) || GlobalsProperties.REAL.equals(serverType)) {
			// 상황에 따른 필터 추가
        }

        // 인코딩 설정
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);
        FilterRegistration.Dynamic characterEncoding = servletContext.addFilter("characterEncodingFilter", characterEncodingFilter);
        characterEncoding.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");

        // 스프링 RestFull 설정
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        FilterRegistration.Dynamic hiddenHttpMethod = servletContext.addFilter("hiddenHttpMethodFilter", hiddenHttpMethodFilter);
        hiddenHttpMethod.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");

        // 파일업로드 설정
        MultipartFilter multipartFilter = new MultipartFilter();
        FilterRegistration.Dynamic multipart = servletContext.addFilter("multipartFilter", multipartFilter);
        multipart.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");

        // String Security 설정
        DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy();
        FilterRegistration.Dynamic springSecurity = servletContext.addFilter("springSecurityFilterChain", springSecurityFilterChain);
        springSecurity.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");



        // 사이트메쉬 설정
        SiteMeshFilter siteMeshFilter = new SiteMeshFilter();
        FilterRegistration.Dynamic siteMeshEncoding = servletContext.addFilter("siteMeshFilter", siteMeshFilter);
        siteMeshEncoding.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD), true, "/*");
        // EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE Filter 영역 EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE




        // SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS Listener 영역 SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS
        // String Core 설정
        servletContext.addListener(new ContextLoaderListener(rootContext));

        // Spring Security 증복로그인 관련 처리 여부(resources/config/spring/context-security.xml session-management->concurrency-control 부분)
        //servletContext.addListener(new HttpSessionEventPublisher());
        // EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE Listener 영역 EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE





        // SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS Servlet 영역 SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS
        // 스프링 MVC 설정
        XmlWebApplicationContext xmlWebApplicationContext = new XmlWebApplicationContext();
        xmlWebApplicationContext.setConfigLocation("classpath:config/spring/servlet-mvc.xml");
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(xmlWebApplicationContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
        /* EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE Servlet 영역 EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE */


	}
}

[web.xml]





	...
	
	
		contextConfigLocation
		classpath:config/spring/context-*.xml
	


	
		monitoring
		net.bull.javamelody.MonitoringFilter
	
	
		monitoring
		/*
	
	
		net.bull.javamelody.SessionListener
	
	
	
		encodingFilter
		org.springframework.web.filter.CharacterEncodingFilter
		
			encoding
			UTF-8
		
		
			forceEncoding
			true
		
	
	
		encodingFilter
		/*
	
	
		org.springframework.security.web.session.HttpSessionEventPublisher
	
	
		httpMethodFilter
		org.springframework.web.filter.HiddenHttpMethodFilter
	
	
		httpMethodFilter
		/*
	
	
		multipartFilter
		org.springframework.web.multipart.support.MultipartFilter
	
	
		multipartFilter
		/*
	
	
		springSecurityFilterChain
		org.springframework.web.filter.DelegatingFilterProxy
	
	
		springSecurityFilterChain
		/*
	
	
		sitemesh
		com.opensymphony.sitemesh.webapp.SiteMeshFilter
	
	
		sitemesh
		/*
		REQUEST
		FORWARD
	
	
		org.springframework.web.context.ContextLoaderListener
	
	
		action
		org.springframework.web.servlet.DispatcherServlet
		
			contextConfigLocation
			classpath:config/spring/servlet-mvc.xml
		
		1
	
	
		action
		/
	


[SampleDaoCase.java]

import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;

import java.util.List;

import javax.annotation.Resource;

import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath*:config/spring/context-*.xml"})
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SampleDaoCase {
	private Logger log = LoggerFactory.getLogger(SampleDaoCase.class);
	@Resource SampleDao sampleDao;

	private String name = "이름 테스트";

	@Test
	public void seq01_addSample_글_등록() {
		Sample sampleVo = new Sample();
		sampleVo.setName(name);
		sampleVo.setEmail("ddakkerABC@gmail.com");

		int insertCnt = sampleDao.addSample(sampleVo);
		log.debug("insertCnt: {}", insertCnt);

		assertThat("글 등록 갯수", 1, is(insertCnt));

	}

	@Test
	public void seq02_getSamplesXml_조회() {
		log.debug("sampleDao: " + sampleDao);
		Sample sampleVo = new Sample();
		sampleVo.setEmail("ddakker@gmail.com");

		List list = sampleDao.getSamplesXml(sampleVo);
		log.debug("list: {}", list);
		log.debug("seq: {}", list.get(0).getSeq());
		log.debug("name: {}", list.get(0).getName());
		if( list.get(0).getName() == null )	log.debug("name NULL");
		else								log.debug("name NOT NULL: {}", list.get(0).getName().length());

		assertThat("리스트 갯수 체크", 0, not(list.size()));
		assertThat("리스트 첫번째 배열의 네임값 체크", list.get(0).getName(), not(nullValue()));

		assertThat("리스트 첫번째 배열의 네임값 체크 다른 방법", list.get(0),hasProperty("name", not(nullValue())));

	}

	@Test
	public void seq03_getSamplesXml_조회_검색() {
		log.debug("sampleDao: " + sampleDao);
		Sample sampleVo = new Sample();
		sampleVo.setEmail("ddakker@gmail.com");
		sampleVo.setSeq(30);

		List list = sampleDao.getSamplesXml(sampleVo);
		log.debug("list: {}", list);
		log.debug("seq: {}", list.get(0).getSeq());
		log.debug("name: {}", list.get(0).getName());
		if( list.get(0).getName() == null )	log.debug("name NULL");
		else								log.debug("name NOT NULL: {}", list.get(0).getName().length());

		assertThat("리스트 갯수 체크", 0, not(list.size()));
		assertThat("리스트 첫번째 배열의 네임값 체크", list.get(0).getName(), not(nullValue()));
		assertThat("리스트 첫번째 배열의 네임값 체크 다른 방법", list.get(0),hasProperty("name", not(nullValue())));

	}

	@Test
	public void seq04_deleteSample_삭제() {
		Sample sampleVo = new Sample();
		sampleVo.setName(name);

		Sample sample = sampleDao.getSample(sampleVo);

		assertThat("글", sample, not(nullValue()));
		assertThat("글번호", sample.getSeq(), not(nullValue()));

		sampleVo = new Sample();
		sampleVo.setSeq(sample.getSeq());

		int deleteCnt = sampleDao.deleteSample(sampleVo);
		assertThat("글 삭제 갯수", 1, is(deleteCnt));
	}

}

[SampleControllerCase.java]

import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import pe.kr.ddakker.core.support.web.tags.pagination.Paging;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextHierarchy({
    @ContextConfiguration("classpath*:config/spring/context-*.xml"),
    @ContextConfiguration("file:webapp/WEB-INF/config/springmvc/name-servlet.xml")
})
public class SampleControllerCase {
	private Logger log = LoggerFactory.getLogger(SampleControllerCase.class);

	@Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
    	this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }


	@Test
	public void listCase_페이징_리스트() throws Exception{
		System.out.println("mockMvc1: " + mockMvc);
		MvcResult mvcResult = this.mockMvc.perform(get("/sample/list").param("a", "1"))
					.andDo(print())
					.andExpect(status().isOk())
					.andExpect(model().attributeExists("paging"))
					.andExpect(view().name("sample/list"))
					.andReturn();

		Paging samplePaging = (Paging) mvcResult.getModelAndView().getModelMap().get("paging");

		assertThat("게시물 정상 여부", samplePaging, not(nullValue()));
		assertThat("게시물 리스트 정상 여부", samplePaging.getList(), not(nullValue()));

		if( samplePaging.getList().size() > 0 ){
			log.debug("seq={}, name={}", samplePaging.getList().get(0).getSeq(), samplePaging.getList().get(0).getName());
			assertThat("리스트 첫번째 배열의 네임값 체크 다른 방법", samplePaging.getList().get(0),hasProperty("name", not(nullValue())));
		}
	}

	@Test
	public void listCase_페이징_리스트_JSON() throws Exception{
		System.out.println("mockMvc1: " + mockMvc);
		this.mockMvc.perform(get("/sample/list").accept(MediaType.APPLICATION_JSON))
					.andDo(print())
					.andExpect(status().isOk())
					.andExpect(jsonPath("$.result").value("0000"))
					;
	}

}


dependencies {
	compile 'org.mybatis:mybatis:3.2.7'
	compile 'org.mybatis:mybatis-spring:1.2.2'
	testCompile 'junit:junit:4.11'
	testCompile 'org.hamcrest:hamcrest-all:1.3'
	testCompile 'com.jayway.jsonpath:json-path-assert:0.9.1'
	testCompile 'org.springframework:spring-test:3.2.9.RELEASE'
}

[TestSchedule.java]


import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class TestSchedule {

	@Scheduled(cron = "*/5 * * * * *")	// second, minute, hour, day, month, weekday
	public void test() throws InterruptedException{
		System.out.println("-- test");
	}

	@Scheduled(fixedDelay=1000)			// 이전에 실행된 Task의 종료 시간부터
	public void test2() throws InterruptedException{
		Thread.sleep(5000);
		System.out.println("-- test2");
	}

	@Scheduled(fixedRate=1000)			// 이전에 실행된 Task의 시작 시간부터
	public void test3() throws InterruptedException{
		Thread.sleep(5000);
		System.out.println("-- test3");
	}
}

[applicationContext.xml]





	
	
	

[web.xml]





	Board

	
	
		contextConfigLocation
		classpath:config/applicationContext*.xml
	
	
	
		org.springframework.web.context.ContextLoaderListener
	



[build.gradle]


apply plugin: 'java'
apply plugin: 'eclipse'

sourceCompatibility = 1.7
version = '1.0'
jar {
    manifest {
        attributes 'Implementation-Title': 'Issue', 'Implementation-Version': version
    }
}

repositories {
    mavenCentral()
}

// 의존성 설정에 사용할 프로퍼티
springVersion = '3.2.0.RELEASE'
slf4jVersion = '1.7.5'
logbackVersion = '1.0.13'

// 메이븐 Central 저장소 사용
repositories {
    mavenCentral()
}

List loggers = [
    "ch.qos.logback:logback-classic:1.0.13",
    "org.slf4j:jcl-over-slf4j:1.7.5",
]

// 의존성 설정
dependencies {
    compile "org.springframework:spring-webmvc:$springVersion"

    compile "commons-lang:commons-lang:2.6"
    compile "commons-httpclient:commons-httpclient:3.1"

	compile "net.sf.json-lib:json-lib:2.2.1:jdk15"
    compile "org.jsoup:jsoup:1.7.3"
    compile "com.taskadapter:redmine-java-api:1.23"

    compile loggers

    testCompile "org.springframework:spring-test:$springVersion"
    testCompile 'junit:junit:4.8.2'
    testCompile 'org.mockito:mockito-core:1.9.0'
}

// commons-logging, log4j, jul 의존성 제거
configurations {
    all.collect { configuration ->
        configuration.exclude group: 'commons-logging', module: 'commons-logging'
        configuration.exclude group: 'log4j', module: 'log4j'
    }
}

uploadArchives {
    repositories {
       flatDir {
           dirs 'repos'
       }
    }
}


ViewResolver 중에 ContentNegotiatingViewResolver 에대한 메모를 한다.

아마 활용도가 가장 많을듯 싶다.


기존에 "Spring Security(스프링 시큐리티) Exception(인증, 허가)에 따른 공통 처리" 에서 살짝 소스로만 남겨놨었는데 그때는 호출 경로(확장자)에 따른 View선택 방식이였다.

(http://localhost/test.do HTML 데이터 리턴, http://localhost/test.json JSON 데이터 리턴, http://localhost/test.xml XML 데이터 리턴)


그런데 위 Security 셈플링을 하였을때는 더 세부적으로 하지 않아서 몰랐었는데 인증 및 권한획득 과정에서 문제가 발생하였다.


예를 들어 Security를 이용하여 로그인을 시도 하였을때, 인증 실패등의 Exception 상황에 맞는(html,json,xml)을 정보를 리턴해주어야 하는데 Security에서 제공하는 "http://..../j_spring_security_check" 에서는 json, xml 확장자를 붙일수 없다는것이었다.

form-login 의 login-processing-url 속성으로 변경은 가능하지만 현재 do, json, xml 등 1개 이상 등록이 설정파일에서는 못 하는것 같았다.(나만 모를수도...)


그래서 확장자를 통한 방법이 아닌 mediaType 따른 분기 방법을 셈플링 해보았다.

인터넷 상에 MediaTypes 를 ContentNegotiationManagerFactoryBean 에 바로 setter 하는 셈플들이 많을텐데, 3.2 버전인가 어디에서 충돌 버그인가 머시기인가 문제가 있어서 최근 버전에서는 setMediaTypes가 @Deprecated 되었다고 한다.

만약 문제 발생시 디컴파일러를 통해서 해당 함수의 @Deprecated 여부를 확인해보세요!!


MediaType 방식을 사용할경우 Request Header 정보에 따라서 뷰를 결정하게 된다.

Accept 값이 = application/json ....

application/xml ....

= text/html ... 등등등


이러한 값들은 jQuery를 사용할 경우 별도 셋팅이 필요 없이, jQuery.ajax({dataType: "json"....)  값에 따라 자동 셋팅됩니다.



[servlet-context.xml]
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
 
    <mvc:annotation-driven />
    <context:component-scan base-package="pe.kr.ddakker" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
        <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" />
    </context:component-scan>
 
    <mvc:interceptors>
        <bean class="pe.kr.ddakker.dakmoney.web.servlet.interceptor.TestInterceptor"/>
    </mvc:interceptors>
 
    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    	<property name="order" value="1" />
	    <property name="contentNegotiationManager">
	        <bean class="org.springframework.web.accept.ContentNegotiationManager">
	        	<constructor-arg>
		            <bean class="org.springframework.web.accept.HeaderContentNegotiationStrategy">
		            </bean>
	            </constructor-arg>
	        </bean>
	    </property>
	    <property name="defaultViews">
	        <list>
	            <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />
	        </list>
	    </property>
	</bean>

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

spring 3.2.9 에서 테스트

Spring Security 를 이용해서 셈플링을 해보면 인증 및 허가에 대한 Exception 이 발생시 LoginForm으로 Redirect 하게 된다.


하지만 나는 그렇게 하고 싶지 않다!!!

일반적인 경우에는 별도의 페이지로 분기하고 싶고, AJAX나 외부 통신시에는 해당 메시지를 XML이나 JSON으로 리턴해주고 싶다!!!


방법을 고민해보다가.. 책도 찾아보고, 인터넷도 뒤져보고.. 방법을 찾아 적어놔본다...




위 그림에서와 같이 ExceptionTranslationFilter 에서 인증 및 권한에 대한 검사를 한다고 한다.


인증이 되지 않았을 경우(비로그인)에는 AuthenticationEntryPoint 부분에서 AuthenticationException 을 발생 시키고, 인증(로그인)은 되었으나 해당 요청에 대한 권한이 없을 경우에는 AccessDeniedHandler 부분에서 AccessDeniedException 이 발생된다.


그렇다면 AuthenticationEntryPoint  부분과 AccessDeniedHandler 부분을 재정의해서 처리해주면 될듯하다.


나의 경우 재정의 한부분에서 특정 Controller 로 Forward 처리 했다.

호출 케이스별로 jsp View, xml, json 으로 출력해줘야 하는데, 재정의 한부분에서 출력해주는 것보다 특정 Controller Method로 하게 되면 MVC 공통으로 처리한것으로 인해 알아서 케이스별로 출력될것이기에...




[build.gradle]

apply plugin: 'war'
apply plugin: 'eclipse-wtp'
apply plugin: 'eclipse'

// JAVA Version 1.6
sourceCompatibility = '1.6'
targetCompatibility = '1.6'

eclipse {
    wtp {
        component {
            contextPath = "/"
        }
        facet {
            facet name: 'jst.web', version: '2.5'
            facet name: 'jst.java', version: '1.6'
        }
    }
}

// 의존성 설정에 사용할 프로퍼티
springVersion = '3.2.0.RELEASE'
springSecurityVersion = '3.1.3.RELEASE'
slf4jVersion = '1.7.5'
logbackVersion = '1.0.13'

// 메이븐 Central 저장소 사용
repositories {
    mavenCentral()
}

List loggers = [
    "ch.qos.logback:logback-classic:1.0.13",
    "org.slf4j:jcl-over-slf4j:1.7.5",
]

// 의존성 설정
dependencies {
	compile "org.springframework:spring-webmvc:$springVersion"
	compile "org.springframework:spring-oxm:$springVersion"

    compile "org.springframework.security:spring-security-web:$springSecurityVersion"
    compile "org.springframework.security:spring-security-taglibs:$springSecurityVersion"
    compile "org.springframework.security:spring-security-config:$springSecurityVersion"

    compile "cglib:cglib-nodep:2.2.2"
	compile "javax.servlet:jstl:1.2"

	compile "org.codehaus.jackson:jackson-mapper-asl:1.9.13"
	compile "net.sf.json-lib:json-lib:2.2.1:jdk15"
	compile "net.sf.json-lib:json-lib-ext-spring:1.0.2"
	compile "xom:xom:1.2.5"
	compile "commons-lang:commons-lang:2.6"

	compile loggers

    testCompile "org.springframework:spring-test:$springVersion"
	testCompile 'junit:junit:4.8.2'
	testCompile 'org.mockito:mockito-core:1.9.0'
}

// commons-logging, log4j, jul 의존성 제거
configurations {
    all.collect { configuration ->
        configuration.exclude group: 'commons-logging', module: 'commons-logging'
        configuration.exclude group: 'log4j', module: 'log4j'
    }
}


[application-context-security.xml]

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:security="http://www.springframework.org/schema/security"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/security
                        http://www.springframework.org/schema/security/spring-security.xsd">

    <security:http auto-config="true" use-expressions="true" entry-point-ref="authenticationEntryPoint">
        <!-- <security:access-denied-handler error-page="/accessIsDenied" /> -->
        <security:access-denied-handler ref="acessDeniedHandler" />
        <security:intercept-url pattern="/login" access="permitAll" />
        <security:intercept-url pattern="/accessIsDenied*" access="permitAll" />
        <security:intercept-url pattern="/isNotLogin*" access="permitAll" />
        <security:intercept-url pattern="/admin*" access="hasRole('admin')" />
        <security:intercept-url pattern="/**" access="hasRole('user')" />
        <security:form-login login-page="/login" default-target-url="/" />
        <security:logout logout-url="/logout" logout-success-url="/login" invalidate-session="true" />
    </security:http>

	<!-- 권한 없는 요청일 경우 -->
	<bean id="acessDeniedHandler" class="pe.kr.ddakker.framework.security.web.access.AccessDeniedHandlerImpl">
		<property name="redirect" value="false" />
		<property name="errorPage" value="/assessDenied.do" />
	</bean>
	
	<!-- 인증전 일 경우 -->
	<bean id="authenticationEntryPoint" class="pe.kr.ddakker.framework.security.web.AuthenticationEntryPointImpl">
		<property name="redirect" value="false" />
		<property name="errorPage" value="/isNotLogin.do" />
	</bean>
    <bean id="encoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder"/>

    <security:authentication-manager>
        <security:authentication-provider>
            <security:password-encoder ref="encoder"/>
            <security:user-service>
                <!-- Password: "koala" for both -->
                <security:user name="user"
                      password="4efe081594ce25ee4efd9f7067f7f678a347bccf2de201f3adf2a3eb544850b465b4e51cdc3fcdde"
                      authorities="user"/>
                <security:user name="admin"
                      password="4efe081594ce25ee4efd9f7067f7f678a347bccf2de201f3adf2a3eb544850b465b4e51cdc3fcdde"
                      authorities="admin"/>
            </security:user-service>
        </security:authentication-provider>
    </security:authentication-manager>
</beans>


[AccessDeniedHandlerImpl.java]

package pe.kr.ddakker.framework.security.web.access;

import static pe.kr.ddakker.framework.web.servlet.view.ServletHelper.convertorViewTypeErrorPage;

import java.io.IOException;

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

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

/**
 * 권한이 없는 요청시 발생
 * @auther ddakker 2013. 12. 6.
 */
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
	private Boolean redirect = true;
	public Boolean getRedirect() {
		return redirect;
	}
	public void setRedirect(Boolean redirect) {
		this.redirect = redirect;
	}

	private String errorPage;
	public void setErrorPage(String errorPage) {
		this.errorPage = errorPage;
	}
	public String getErrorPage() {
		return errorPage;
	}

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
		// 에러 페이지에 대한 확장자를 현재 호출한 확장자와 마추어준다.
		String goErrorPage = convertorViewTypeErrorPage(request, errorPage);
		if( redirect ){
			response.sendRedirect(goErrorPage);
		}else{
			RequestDispatcher dispatcher = request.getRequestDispatcher(goErrorPage);
			dispatcher.forward(request, response);
		}
	}

}


[AuthenticationEntryPointImpl.java]

package pe.kr.ddakker.framework.security.web;

import static pe.kr.ddakker.framework.web.servlet.view.ServletHelper.convertorViewTypeErrorPage;

import java.io.IOException;

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

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

/**
 * 인증하지 않은 상황에서 호출시 발생
 * @auther ddakker 2013. 12. 6.
 */
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
	private Boolean redirect = true;
	public Boolean getRedirect() {
		return redirect;
	}
	public void setRedirect(Boolean redirect) {
		this.redirect = redirect;
	}

	private String errorPage;
	public void setErrorPage(String errorPage) {
		this.errorPage = errorPage;
	}
	public String getErrorPage() {
		return errorPage;
	}

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
		// 에러 페이지에 대한 확장자를 현재 호출한 확장자와 마추어준다.
		String goErrorPage = convertorViewTypeErrorPage(request, errorPage);
		if( redirect ){
			response.sendRedirect(goErrorPage);
		}else{
			RequestDispatcher dispatcher = request.getRequestDispatcher(goErrorPage);
			dispatcher.forward(request, response);
		}
	}

}


[servlet-context.xml]

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

	<mvc:annotation-driven />
	<context:component-scan base-package="pe.kr.ddakker" use-default-filters="false">
		<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
		<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" />
	</context:component-scan>

	<mvc:interceptors>
        <bean class="pe.kr.ddakker.dakmoney.web.servlet.interceptor.TestInterceptor"/>
    </mvc:interceptors>

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

	
	<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
	    <property name="contentNegotiationManager">
	        <bean class="org.springframework.web.accept.ContentNegotiationManager">
	            <constructor-arg>
	            <bean
	                class="org.springframework.web.accept.PathExtensionContentNegotiationStrategy">
	                <constructor-arg>
	                    <map>
	                        <entry key="xml" value="application/xml" />
	                        <entry key="json" value="application/json" />
	                        <entry key="jsonp" value="application/javascript" />
	                    </map>
	                </constructor-arg>
	            </bean>
	            </constructor-arg>
	        </bean>
	    </property>
	    <property name="defaultViews">
	        <list>
	            <bean class="pe.kr.ddakker.framework.web.servlet.view.xml.MappingXmlView">
	            	<property name="contentType" value="application/xml"/>
	            </bean>
	            <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
	            	<property name="contentType" value="application/json"/>
	            </bean>

	            <bean class="pe.kr.ddakker.framework.web.servlet.view.json.MappingJsonpView">
	                <property name="contentType" value="application/javascript"/>
	            </bean>
	        </list>
	    </property>
	    <property name="ignoreAcceptHeader" value="false" />
	</bean>
</beans>


[HomeController.java]

package pe.kr.ddakker.dakmoney;

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import pe.kr.ddakker.framework.web.servlet.exception.SessionNotException;

@Controller
public class HomeController {

	private final Logger logger = LoggerFactory.getLogger(getClass());

	/**
	 * Simply selects the home view to render by returning its name.
	 */
	@RequestMapping(value = "/")
	public String index(Locale locale, Model model) {
		logger.info("Welcome home! The client locale is {}.", locale);
		HomeBean helloBean = new HomeBean();
		helloBean.setNm("이름 index");
		helloBean.setAge(22);
		model.addAttribute("helloBean", helloBean);

		Date date = new Date();
		DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

		String formattedDate = dateFormat.format(date);

		model.addAttribute("serverTime", formattedDate + " Hello 안녕 하세요" );

		return "home";
	}

	/**
	 * Simply selects the home view to render by returning its name.
	 * @throws Exception
	 */
	@RequestMapping(value = "/json", method = RequestMethod.GET)
	public String index2(Locale locale, Model model) throws Exception {
		logger.info("Welcome home! The client locale is {}.", locale);
		HomeBean helloBean = new HomeBean();
		helloBean.setNm("이름 index2");
		helloBean.setAge(22);
		model.addAttribute("helloBean", helloBean);

		Date date = new Date();
		DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

		String formattedDate = dateFormat.format(date);

		model.addAttribute("serverTime", formattedDate + " Hello 안녕 하세요" );

		return "home";
	}

	/**
	 * Simply selects the home view to render by returning its name.
	 * @throws Exception
	 */
	@RequestMapping(value = "/json2", method = RequestMethod.GET)
	public String index22(Locale locale, Model model) throws Exception {
		logger.info("Welcome home! The client locale is {}.", locale);
		HomeBean helloBean = new HomeBean();
		helloBean.setNm("이름 index22");
		helloBean.setAge(22);
		model.addAttribute("helloBean", helloBean);

		Date date = new Date();
		DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

		String formattedDate = dateFormat.format(date);
		if( true ){
			throw new RuntimeException("와우");
		}

		model.addAttribute("serverTime", formattedDate + " Hello 안녕 하세요" );

		return "home";
	}

	@RequestMapping(value = "/login", method = RequestMethod.GET)
	public String login(Locale locale, Model model) throws Exception {
		logger.debug("loginForm");
		return "loginForm";
	}

	@RequestMapping(value = "/isNotLogin", method = RequestMethod.GET)
	public void isNotLogin(Model model, @RequestParam Map map) throws Exception {
		logger.debug("---------- isNotLogin()");
		logger.debug("---------- isNotLogin() params: " + map);
		if( true ) throw new SessionNotException("로그인 되지 않았습니다.");
	}

	@RequestMapping(value = "/assessDenied", method = RequestMethod.GET)
	public String assessDenied(Locale locale, Model model) throws Exception {
		logger.debug("---------- assessDenied()");
		if( true ) throw new SessionNotException("권한이 없는 요청입니다.");
		return "assessDenied";
	}

	@RequestMapping(value = "/admin", method = RequestMethod.GET)
	public String admin(Locale locale, Model model) throws Exception {
		logger.debug("---------- admin()");
		return "admin";
	}
}


[DefaultControllerAdvice.java]

package pe.kr.ddakker.dakmoney;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

import pe.kr.ddakker.framework.web.servlet.exception.SessionNotException;

@ControllerAdvice
public class DefaultControllerAdvice {
	private final Logger logger = LoggerFactory.getLogger(DefaultControllerAdvice.class);

	@ExceptionHandler(Exception.class)
	public ModelAndView handleException(HttpServletRequest request, Exception ex) {
		logger.error("에러임 Exception: {}", ex.toString());
		printExceptionInfo(request, ex);
        ModelAndView mv = new ModelAndView("error");
        mv.addObject("result", "9999");
        mv.addObject("message", ex.getMessage());
        return mv;
    }

	@ExceptionHandler(RuntimeException.class)
	public ModelAndView handleRuntimeException(HttpServletRequest request, RuntimeException ex) {
		logger.error("에러임 RuntimeException: {}", ex.toString());
		printExceptionInfo(request, ex);
        ModelAndView mv = new ModelAndView("error");
        mv.addObject("result", "8888");
        mv.addObject("message", ex.getMessage());
        return mv;
    }
	
	@ExceptionHandler(SessionNotException.class)
	public ModelAndView handleSessionNotException(HttpServletRequest request, SessionNotException ex) {
		logger.error("에러임 SessionNotException: {}", ex.toString());
		printExceptionInfo(request, ex);
        ModelAndView mv = new ModelAndView("error");
        mv.addObject("result", "7777");
        mv.addObject("message", ex.getMessage());
        return mv;
    }
	
	private void printExceptionInfo(HttpServletRequest request, Exception ex){
		logger.error("PARMAS: {}", request.getParameterMap());
		for( StackTraceElement s : ex.getStackTrace() ){
			logger.error("STACK: {}", s);
		}
	}

}


이전 버전에는 개별적인 Controller에서 @ExceptionHandler 를 활용해서 Exception을 모았었다.


공통화를 위해 고민해보다가 @ExceptionHandler 함수 구현을 상위 클래스에 두고 Controller에서 해당 클래스를 모조건 상속을 받게끔 하는 방법으로 해보았었는데.. 좀 찜짐한감이 없잖아 있었다..(불필요한 코딩 한자라도 줄이고 싶었다.., 또한 특정 템플릿을 강요하고 싶지도 않았다..)


아니면 Interceptor 에서 Exception을 Cache 하여 처리하는 방법으로 했다던가..


셈플링 해보던중.. @ControllerAdvice를 발견!!


상속, Interceptor 없이 모든 Exception을 모을 수 있다!!



@ControllerAdvice
public class DefaultControllerAdvice {
	private static final Logger logger = LoggerFactory.getLogger(DefaultControllerAdvice.class);

	@ExceptionHandler(RuntimeException.class)
	public ModelAndView handleRuntimeException(RuntimeException ex) {
		logger.error("에러임 RuntimeException: {}", ex);
        ModelAndView mv = new ModelAndView("error");
        mv.addObject("result", "999");
        mv.addObject("message", ex.getMessage());
        return mv;
    }

}


	
	<mvc:annotation-driven />
	<context:component-scan base-package="pe.kr.ddakker" use-default-filters="false">
		<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
		<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" />
	</context:component-scan>

propagation="REQUIRED" read-only="true"


Spring AOP Transaction 과 관련하여 와와 같이 설정 하였을 경우 해당 설정이 적용되는 트랜잭션의 경우 insert/update/delete가 일어난다면 ".. TransientDataAccessResourceException .. java.sql.SQLException: Connection is read-only. .." 와 같은 예외가 나면서 중지되어야 한다고 인터넷, 책 등등에 설명이 잘 되어있다.


하지만... 안됐다.. 개삽질 했다.. 몇시간을 삽질한지 모르겠다..

별 셈플링을 다 해봤다.. 안됐다..


그런데 DB가 MYSQL로 셈플링 한 프로젝트에서 동일하게 적용해보았으나 잘 되었다.


이것저것 찾아보니 DBMS Vendor따라서.. 안될 수도 있단다..



[참고] https://groups.google.com/forum/#!searchin/ksug/AOP$20트랜잭션$20설정에$20대해서../ksug/tVLRWx8R_B4/iCL_Cz6VWzMJ

우선 Spring AOP 에서 Proxy 를 사용하는데 두가지가 존재한다고 하네...

  1. JDK Dynamix Proxy    - Interface 가 존재해야 하고(Runtime 시점에 감싸주는거던데... 고로 야야야야야약간의 성능저하.??)
  2. CGLib Proxy             - 구현체만 있으면 됨..(Compile 시점에 감싸므로 성능저하 최소화)


2번 방법으로 사용하려면 <aop:config proxy-target-class="true"> 셋팅이 필요함


ex) 근데 Service Layer에서는 셋팅하지 않아도 정상적이였는데, Dao Layer에서는 문제가 생겼다.. 머징;;


역시 지식이 너무 얕다.. ;; 

삽질 무진장 했네


[참고] https://groups.google.com/forum/?hl=ko&fromgroups=#!topic/ksug/pm2ET851V6U



	
	
	

	

		
			
			
			
			
			
			
		
		

	


  • *-servlet.xml 과 applicationContext.xml 설정 시 주의 점
    1. *-servlet.xml 에서 Controller Annotation 스캔 시 무조껀 Controller 만 함
    2. applicationContext.xml 에서는 Controller 를 제외 하고 스캔함
  • 위와 같이 하지 않을경우 둘의 상-하 상속관계에 따라서 applicationContext.xml 의 Bean이 사용되어지는것이 아니라 *-servlet.xml 의 Bean이 사용되어 진다고 함...
    그러므로 applicationContext.xml 에서 tx(Transaction)관련 설정을 해 놓아도 먹히지 않는 문제가 발생합니다.

[*-servlet.xml]

	
		
	
[applicationContext.xml]


	
		
	


SpringMVC 어노테이션 Controller 에서 ModelAndView 를 이용하여 redirect 했을때 문제가 발생한 경우를 보았다.

문제 발생 상황은 Oracle varchar2(4000) 상황에서 약 2000바이트 문자열의 데이터를 쓰기 할때 쓰기를 정상적으로 완료 후 list 페이지로 redirect 하는 순간 브라우저 오류가 발생했다.(HTTP 에러가 아님)(forward는 정상)

또한, DB insert는 Exception 없이 정상 처리되었다. 

해결방법은 아래와 같았다.
setExposeModelAttributes 함수의 기능은 잘 이해되지 않는다..(HTTP 변수 노출여부 라는데..)


RedirectView rv = new RedirectView("list.do?param=1");
rv.setExposeModelAttributes(false);
return new ModelAndView(rv); 

[2011-06-12] 추가
Redirect 할 경우 Model의 값들이 URL에 Parameter값이 붙을경우 위와 같이 하면 Parameter 을 없앨수 있다. 

+ Recent posts