해외 기업의 웹서비스를 이용하는 기능을 개발하고 있다. 이 과정에서 낯설은 wsdl 생성과정 및 SOAP 사용방법을 정리해보고자 한다.

wsimport 는 JAX-WS 에 적합한 산출물을 생성하는 도구다. wsdl(Web Services Description Language) 을 불러와 그 파일을 기준으로 자바 코드를 생성한다.

사용방법

사용방법은 간단하다(물론 옵션은 여러가지가 있다. 상황에 따라 적절한 옵션을 추가하자).

$ wsimport {wsdl-url}

선택사항

$ wsimport
wsimport
Missing WSDL_URI
Usage: wsimport [options] <WSDL_URI>
where [options] include:
-b <path> specify jaxws/jaxb binding files or additional schemas
(Each <path> must have its own -b)
-B<jaxbOption> Pass this option to JAXB schema compiler
-catalog <file> specify catalog file to resolve external entity references
supports TR9401, XCatalog, and OASIS XML Catalog format.
-classpath <path> specify where to find user class files and wsimport extensions
-cp <path> specify where to find user class files and wsimport extensions
-d <directory> specify where to place generated output files
-encoding <encoding> specify character encoding used by source files
-extension allow vendor extensions - functionality not specified
by the specification. Use of extensions may
result in applications that are not portable or
may not interoperate with other implementations
-help display help
-httpproxy:<proxy> set a HTTP proxy. Format is [user[:password]@]proxyHost:proxyPort
(port defaults to 8080)
-J<javacOption> pass this option to javac
-keep keep generated files
-p <pkg> specifies the target package
-quiet suppress wsimport output
-s <directory> specify where to place generated source files
-target <version> generate code as per the given JAXWS spec version
Defaults to 2.2, Accepted values are 2.0, 2.1 and 2.2
e.g. 2.0 will generate compliant code for JAXWS 2.0 spec
-verbose output messages about what the compiler is doing
-version print version information
-fullversion print full version information
-wsdllocation <location> @WebServiceClient.wsdlLocation value
-clientjar <jarfile> creates the jar file of the generated artifacts along with the
WSDL metadata required for invoking the web service.
-generateJWS generate stubbed JWS implementation file
-implDestDir <directory> specify where to generate JWS implementation file
-implServiceName <name> local portion of service name for generated JWS implementation
-implPortName <name> local portion of port name for generated JWS implementation
Extensions:
-XadditionalHeaders map headers not bound to request or response message to
Java method parameters
-Xauthfile file to carry authorization information in the format
http://username:password@example.org/stock?wsdl
-Xdebug print debug information
-Xno-addressing-databinding enable binding of W3C EndpointReferenceType to Java
-Xnocompile do not compile generated Java files
-XdisableAuthenticator disable Authenticator used by JAX-WS RI,
-Xauthfile option will be ignored if set
-XdisableSSLHostnameVerification disable the SSL Hostname verification while fetching
wsdls
Examples:
wsimport stock.wsdl -b stock.xml -b stock.xjb
wsimport -d generated http://example.org/stock?wsdl

실습

http://www.webservicex.com/globalweather.asmx?WSDL 을 기준으로 테스트를 해보자.

$ wsimport -verbose -keep -extension http://www.webservicex.com/globalweather.asmx\?WSDL

라고 실행하면

parsing WSDL...
[WARNING] SOAP port "GlobalWeatherSoap12": uses a non-standard SOAP 1.2 binding.
line 199 of http://www.webservicex.com/globalweather.asmx?WSDL
[WARNING] Port "GlobalWeatherHttpGet" is not a SOAP port, it has no soap:address
line 202 of http://www.webservicex.com/globalweather.asmx?WSDL
[WARNING] port "GlobalWeatherHttpGet": not a standard SOAP port. The generated artifacts may not work with JAX-WS runtime.
line 202 of http://www.webservicex.com/globalweather.asmx?WSDL
[WARNING] Port "GlobalWeatherHttpPost" is not a SOAP port, it has no soap:address
line 205 of http://www.webservicex.com/globalweather.asmx?WSDL
[WARNING] port "GlobalWeatherHttpPost": not a standard SOAP port. The generated artifacts may not work with JAX-WS runtime.
line 205 of http://www.webservicex.com/globalweather.asmx?WSDL
Generating code...
net/webservicex/GetCitiesByCountry.java
net/webservicex/GetCitiesByCountryResponse.java
net/webservicex/GetWeather.java
net/webservicex/GetWeatherResponse.java
net/webservicex/GlobalWeather.java
net/webservicex/GlobalWeatherHttpGet.java
net/webservicex/GlobalWeatherHttpPost.java
net/webservicex/GlobalWeatherSoap.java
net/webservicex/ObjectFactory.java
net/webservicex/package-info.java
Compiling code...
javac -d /private/tmp/test-ws/. -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/lib/tools.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/classes -Xbootclasspath/p:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre/lib/rt.jar /private/tmp/test-ws/./net/webservicex/GetCitiesByCountry.java /private/tmp/test-ws/./net/webservicex/GetCitiesByCountryResponse.java /private/tmp/test-ws/./net/webservicex/GetWeather.java /private/tmp/test-ws/./net/webservicex/GetWeatherResponse.java /private/tmp/test-ws/./net/webservicex/GlobalWeather.java /private/tmp/test-ws/./net/webservicex/GlobalWeatherHttpGet.java /private/tmp/test-ws/./net/webservicex/GlobalWeatherHttpPost.java /private/tmp/test-ws/./net/webservicex/GlobalWeatherSoap.java /private/tmp/test-ws/./net/webservicex/ObjectFactory.java /private/tmp/test-ws/./net/webservicex/package-info.java

처럼 실행되어 있는 것을 볼 수 있을 것이다. 대상으로 하는 wsdl 파일을 내려받은 후에 이파일을 기준으로 자바코드를 생성하는 과정을 확인할 수 있다. 그리고 내려받은 자바코드를 컴파일하는 것까지 처리해준다.

생성된 디렉토리의 구조는 다음과 같다.

.
├── globalweather.asmx?WSDL
└── net
    └── webservicex
        ├── GetCitiesByCountry.class
        ├── GetCitiesByCountry.java
        ├── GetCitiesByCountryResponse.class
        ├── GetCitiesByCountryResponse.java
        ├── GetWeather.class
        ├── GetWeather.java
        ├── GetWeatherResponse.class
        ├── GetWeatherResponse.java
        ├── GlobalWeather.class
        ├── GlobalWeather.java
        ├── GlobalWeatherHttpGet.class
        ├── GlobalWeatherHttpGet.java
        ├── GlobalWeatherHttpPost.class
        ├── GlobalWeatherHttpPost.java
        ├── GlobalWeatherSoap.class
        ├── GlobalWeatherSoap.java
        ├── ObjectFactory.class
        ├── ObjectFactory.java
        ├── package-info.class
        └── package-info.java


바보, 삽질을 하다.

public class WBAccessDeniedHandler implements AccessDeniedHandler {
 
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOExceptionServletException {
        String redirectUrl = "/";
        response.sendRedirect(redirectUrl);
    }
}

redirectUrl 는 반드시 / 으로 시작해야 한다. 안그러면…​ 접근한 requestUrl 에 끊없이 redirectUrl 붙다가 오류가 난다.


HttpServletResponse 의 sendRedirect 메서드를 사용하다가 다음과 같은 메시지가 나왔다.

java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed
at org.apache.catalina.connector.ResponseFacade.sendRedirect(ResponseFacade.java:494)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at org.springframework.security.web.firewall.FirewalledResponse.sendRedirect(FirewalledResponse.java:41)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendRedirect(OnCommittedResponseWrapper.java:128)
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:138)
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendRedirect(OnCommittedResponseWrapper.java:128)
  // 중략

이런 코드가 발생하는 경우를 찾아보면, 다음처럼 되어 있었다.

String redirectUrl = request.getRequestURI();
 
if(user.getStatus() == UserStatus.EMAIL_VERIFY) {
    response.sendRedirect(URL_SIGNUP_PROGRESS);
}
 
if(request.getRequestURI().contains("login") || request.getRequestURI().contains("sign")) {
    response.sendRedirect(URL_ROOT);
}
 
response.sendRedirect(redirectUrl);

예외 메시지를 살펴보니 sendRedirect 메서드가 호출된 이후에 재호출하려고 하면 java.lang.IllegalStateException: Cannot call sendRedirect() after the response has been committed 가 출력되는 것이다. 호출된 이후에는 다시 호출할 수 없다는 뜻으로 변경할 수 없다는 뜻이다.

이에 대한 해결책을 좀 찾아봤다.

이걸 해결항 수 있는 방법으로는,

String redirectUrl = request.getRequestURI();
 
if(user.getStatus() == UserStatus.EMAIL_VERIFY) {
    response.sendRedirect(URL_SIGNUP_PROGRESS);
} else if(request.getRequestURI().contains("login") || request.getRequestURI().contains("sign")) {
    response.sendRedirect(URL_ROOT);
} else {
    response.sendRedirect(redirectUrl);
}

와 같은 방식으로 완전히 조건문 내에서 한번만 호출되도록 처리하거나,

String redirectUrl = request.getRequestURI();
 
if(user.getStatus() == UserStatus.EMAIL_VERIFY) {
    redirectUrl = URL_SIGNUP_PROGRESS;
}
 
if(request.getRequestURI().contains("login") || request.getRequestURI().contains("sign")) {
    redirectUrl = URL_ROOT;
}
 
response.sendRedirect(redirectUrl);

위의 코드처럼 조건을 모두 마친 후에 마지막에 sendRedirect 를 호출하는 것이다. 마지막 방법이 좀 더 괜찮은 처리방법이라는 생각이…​


the Java API for Microsoft Documents 을 지향하는Apache POI를 이용한 엑셀 생성 및 다운로드 기능 구현을 해보겠다.

현재 배포중인 버전에 대해서는 Maven central Repository 를 통해서 그때그때 확인하기 바란다.

1. 의존성 추가

1.1. pom.xml

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>3.14</version>
</dependency>

1.2. build.gradle

compile ('org.apache.poi:poi:3.14')

2. 간단한 테스트

POI에 대한 의존성을 추가했으면 간단한 예제를 구현하여 엑셀과 관련된 WorkBook(=file), Sheet, Row, Cell 에 대한 기능을 확인하자.

@Slf4j
public class SheetServiceTest {
    /**
     * @throws Exception
     * @see <a href="https://poi.apache.org/spreadsheet/quick-guide.html#NewWorkbook"></a>
     */
    @Test
    public void generateWorkBook() throws Exception {
        List<Dump> dumps = generateDump(10, "a", "b", "c", "d");
        String excelFileName = "test.xls";
        String sheetname = "send-check-1";
 
        Workbook workbook = new HSSFWorkbook();
        workbook.createSheet(sheetname);
 
 
        Sheet sheet = workbook.getSheet(sheetname);
 
        int size = dumps.size();
        Row headerRow = sheet.createRow(0);
        headerRow.createCell(0).setCellValue("A column");
        headerRow.createCell(1).setCellValue("B column");
        headerRow.createCell(2).setCellValue("C column");
        headerRow.createCell(3).setCellValue("D column");
 
        for (int rownum = 0; rownum < size; rownum++) {
            Row row = sheet.createRow(rownum + 1);
            row.createCell(0).setCellValue(dumps.get(rownum).getA());
            row.createCell(1).setCellValue(dumps.get(rownum).getB());
            row.createCell(2).setCellValue(dumps.get(rownum).getC());
            row.createCell(3).setCellValue(dumps.get(rownum).getD());
        }
 
        generateExcel(Paths.get(excelFileName).toString(), workbook);
 
        assertThat(Paths.get(excelFileName).toFile().exists()).isTrue();
        assertThat(workbook.getSheet(sheetname)).isNotNull();
        assertThat(sheet.getLastRowNum()).isEqualTo(10);
        assertThat(sheet.getRow(sheet.getFirstRowNum()).getCell(0).toString()).isEqualTo("A column");
        assertThat(sheet.getRow(sheet.getLastRowNum()).getCell(0).toString()).isEqualTo("a9");
 
        Paths.get(excelFileName).toFile().delete();
    }
 
    private List<Dump> generateDump(int it, String a, String b, String c, String d) {
        List<Dump> dumps = Lists.newArrayList();
        for (int i = 0; i < it; i++) {
            dumps.add(Dump.builder().a(+ "" + i).b(+ "" + i).c(+ "" + i).d(+ "" + i).build());
        }
        return dumps;
    }
 
    private void generateExcel(String filePath, Workbook workbook) {
        log.debug("generate excel: {}", filePath);
        try (FileOutputStream fileOut = new FileOutputStream(filePath);) {
            workbook.write(fileOut);
        } catch (Exception e) {
            log.error("Occur exception: {}", e);
        }
    }
 
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class Dump {
        String a;
        String b;
        String c;
        String d;
    }
}

3. 스프링MVC 에서 구현

스프링에서 제공하던 AbstractExcelView 는 4.2 에서 Deprecated되었다. AbstractXlsView 를 사용해볼까 했는데 apache POI 3.14 버전에서는 XSSFWorkbook 관련한 패키지가 사라져서 쓸 수 없다. 3.16 버전에는 SXSSFWorkbook 관련 패키지가 추가될 것으로 보이니 3.16 쓸 때는 SXSSFWorkbook 사용을 고려해보자. AbstractXlsView를 사용하도록 하자.

3.1. AbstractXlsView 을 구현

@Component("downloadXlsView") // 공통적으로 사용할 수도 있고... 특정 기능에 따라 뷰이름을 구분지어 사용할 수 있겠다. 
public class SendCheckRequestExportXlsView extends AbstractXlsView {
  // 코드는 중략... 상상의 나래를 활짝 펴라~ 
  @Override
    protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request, HttpServletResponse response) throws Exception {
 
    }
}

org.springframework.web.servlet.view.document 패키지에AbstractXlsView 와 AbstractPdfView 가 있다. AbstractXlsxView는 AbstractXlsView 를 확장했는데 import org.apache.poi.xssf.usermodel.XSSFWorkbook 를 필요로 한다. 그런데 POI 3.14 에는 org.apache.poi.xssf 패키지가 빠져있다. 히스토리는 모르겠다.

3.2. Controller 에서 view 를 반환

@Controller
public class DownloadController {
  @RequestMapping("/download-xls")
  public ModelAndView() {
    // view 에서 처리하는데 필요한 데이터를 담아주면 되겠다. 
    return new ModelAndView("downloadXlsView", "model", model);
  }
}

이렇게 하고 이 컨트롤러에 대한 링크를 제공하면 다운로드 처리가 완료된다.

<a href="/download-xls"></a>

4. 정리

엑셀에 필요한 정보를 담기 위해서는 여전히 많은 노력이 필요하다. 뭐 그래도 어떤 데이터들을 출력할지만 정의하면 비교적 처리가 수월해진다.


타임리프의 확장은 쉽다. 방언(다이얼렉트,Dialect)를 생성하고 템플릿엔진에 추가하면 된다.

Dialect

타임리프 다이얼렉트는 템플릿에서 사용할 수 있는 기능이다.

다음과 같이 동작하는 다이얼렉트를 작성해보자.

<p hello:sayto="Jake">Hi ya!</p>

모든 다이얼렉트 는 IDialect 를 구현해야한다. 이를 용이하게 할 수 있도록AbstractDialect 을 이용한다. IProcessorDialect 는 실제로 Dialect 가

HelloDialect
public class HelloDialect extends AbstractDialect implements IProcessorDialect {
    public static final String PREFIX = "hello";
    public static final int PRECEDENCE = 501;
 
    public TimezoneDateDialect() {
        super("hello-dialect");
    }
 
    @Override
    public String getPrefix() {
        return PREFIX;
    }
 
    @Override
    public int getDialectProcessorPrecedence() {
        return PRECEDENCE;
    }
 
    @Override
    public Set<IProcessor> getProcessors(String dialectPrefix) {
        Set<IProcessor> iProcessors = Sets.newHashSet()
        iProcessors.add(new SayToProcessor(dialectPrefix)); (1)
        return iProcessors;
    }
}
뭔가~ 다른 것들을 추가할 수 있을 것 같지 않은가??

그럼 이제 이 다이얼렉트가 호출하여 실체 로직처리를 수행할 프로세스를 작성해보자.

SayToProcessor
public class SayToProcessor extends AbstractStandardExpressionAttributeTagProcessor {
 
    public static final String ATTR_NAME = "sayTo";
    public static final int PRECEDENCE = 1501;
 
    public SayToProcessor(String dialectPrefix) {
        super(TemplateMode.HTML, dialectPrefix, ATTR_NAMEPRECEDENCEtrue);
    }
 
    @Override
    protected void doProcess(ITemplateContext contextIProcessableElementTag tagAttributeName attributeNameString attributeValueObject expressionResultIElementTagStructureHandler structureHandler) {
        //TODO 비즈니스로직처리!! 
        structureHandler.setBody("Hello, " + expressionResult, false);
    }
}

작성한 다이얼렉트 를 템플릿엔진에 추가하자.

@Bean
public SpringTemplateEngine templateEngine() {
    SpringTemplateEngine engine = new SpringTemplateEngine();
    engine.setTemplateResolver(templateResolver());
    engine.setMessageSource(messageSource);
    engine.addDialect(new HelloDialect());
    return engine;
}

을 구현하고 나면!! 끝이 난다. 실제로 화면을 불러와 보면

<p>HelloJake!</p>

으로 출력되는 것을 볼 수 있을 것이다.


+ Recent posts