Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!

Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 
Показаны сообщения с ярлыком jetty. Показать все сообщения
Показаны сообщения с ярлыком jetty. Показать все сообщения

вторник, 17 июня 2014 г.

Как мокать Bean'ы внутри Spring context запущеного Jetty? Part 2

Есть Web приложение на Spring (MVC + IoC). Есть Jetty сервер, на котором оно ранится. Web приложение в своей Model содержит ряд сервисов (Spring Beans), которыми пользуются контроллера.

Задача: Написать функциональный тест с испольованием WebDriver, но так, чтобы из теста была возможность мокать реальные сервиса модели.

В прошлый раз задача решилась, но как показала практика - сервиса, которые autowired'ся в другие сервиса не мокаются, мокаются только бины, которые попадают во внешние классы, например, вебконтроллеры, или при получении бина из context.getBean("name");

Попробуем решить и эту задачку. Допустим есть два бина:
package com.services;

import org.springframework.stereotype.Component;

@Component("otherService")
public class OtherService {
    
    public String getOtherString() {
        return "Hello from " + OtherService.class.getSimpleName() + "!";
    }
}
И
package com.services;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component("service")
public class Service {

    @Autowired
    private OtherService otherService;

    public String getString() {
        return "Hello from " + Service.class.getSimpleName() + "! " + otherService.getOtherString();
    }
}
Как видно по исходному коду - они зависимы друг от дружки.

Есть еще SpringMVC контроллер
package com.controller;

import com.services.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/")
public class SomeController {

    @Autowired
    private Service service;

    @RequestMapping(method = RequestMethod.GET)
    public String action(Model model) {
        model.addAttribute("string", service.getString());
        return "view";
    }
}
Все как видно работает на аннотациях, о чем указано в идеально чистом applicationContext.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"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.services"/>

</beans>
Есть и вьюшка (но это не так важно, потому как она всего лишь выводит в html то, что ей передали)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;">
    <title>Main page</title>
</head>
<body>
    <c:if test="${string!=null}">
        <span id="message">${string}</span>
    </c:if>
<body>
</html>
Есть еще два конфига servlet-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
            http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.controller"/>

    <mvc:annotation-driven/>

    <mvc:resources mapping="/resources/**" location="/resources/" />

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

</beans>
Ну и конечно же сердце web приложения web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">
    <display-name>Mocker</display-name>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value>
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:com/applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

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

    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>
И как теперь сделать так, чтобы я в тестах, которые пишу мог мокать любой из бинов, при этом поднимая приложение на реальном allicationContext.xml не влезая тестами в продакшен код? Оказывается (спустя 5 часов ночного дебага) не так уж и сложно. История начинается с небольшого интерфейса BeanPostProcessor, реализовав который можно встроиться в процесс инициализации каждого бина в системе. Он прост.
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.beans.factory.config;

import org.springframework.beans.BeansException;

/**
 * Factory hook that allows for custom modification of new bean instances,
 * e.g. checking for marker interfaces or wrapping them with proxies.
 *
 * ApplicationContexts can autodetect BeanPostProcessor beans in their
 * bean definitions and apply them to any beans subsequently created.
 * Plain bean factories allow for programmatic registration of post-processors,
 * applying to all beans created through this factory.
 *
 * Typically, post-processors that populate beans via marker interfaces
 * or the like will implement {@link #postProcessBeforeInitialization},
 * while post-processors that wrap beans with proxies will normally
 * implement {@link #postProcessAfterInitialization}.
 *
 * @author Juergen Hoeller
 * @since 10.10.2003
 * @see InstantiationAwareBeanPostProcessor
 * @see DestructionAwareBeanPostProcessor
 * @see ConfigurableBeanFactory#addBeanPostProcessor
 * @see BeanFactoryPostProcessor
 */
public interface BeanPostProcessor {
    /**
     * Apply this BeanPostProcessor to the given new bean instance <i>before</i> any bean
с * initialization callbacks (like InitializingBean's <code>afterPropertiesSet</code>
     * or a custom init-method). The bean will already be populated with property values.
     * The returned bean instance may be a wrapper around the original.
     * @param bean the new bean instance
     * @param beanName the name of the bean
     * @return the bean instance to use, either the original or a wrapped one; if
     * <code>null</code>, no subsequent BeanPostProcessors will be invoked
     * @throws org.springframework.beans.BeansException in case of errors
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
     */
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
    
        /**
     * Apply this BeanPostProcessor to the given new bean instance <i>after</i> any bean
     * initialization callbacks (like InitializingBean's <code>afterPropertiesSet</code>
     * or a custom init-method). The bean will already be populated with property values.
     * The returned bean instance may be a wrapper around the original.
     * In case of a FactoryBean, this callback will be invoked for both the FactoryBean
     * instance and the objects created by the FactoryBean (as of Spring 2.0). The
     * post-processor can decide whether to apply to either the FactoryBean or created
     * objects or both through corresponding <code>bean instanceof FactoryBean</code> checks.
     * This callback will also be invoked after a short-circuiting triggered by a
     * {@link InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation} method,
     * in contrast to all other BeanPostProcessor callbacks.
     * @param bean the new bean instance
     * @param beanName the name of the bean
     * @return the bean instance to use, either the original or a wrapped one; if
     * <code>null</code>, no subsequent BeanPostProcessors will be invoked
     * @throws org.springframework.beans.BeansException in case of errors
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
     * @see org.springframework.beans.factory.FactoryBean
     */
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
Один его метод postProcessAfterInitialization - нам и нужен. Именно в нем можно обернуть тот или иной бин в Mockito.spy() и получить желаемый результат. Чтобы BeanPostProcessor заработал, его так же надо (либо аннотациями либо через applicationContext.xml) засветить перед Spring.
@Component // вот без этого пока никак! Процессор должен быть виден для Spring
public class SpyPostProcessor implements BeanPostProcessor, Ordered {

        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (spy.equals("service")) { // как-то так
                return Mockito.spy(bean);
            } else {
                return bean;
            }
        }

        @Override
        public int getOrder() {
            return Ordered.LOWEST_PRECEDENCE; // наивысший приоритет у этого BeanPostProcessor
        }
    }
Так же желательно реализовать еще один интерфейс, чтобы указать порядок обработки бина в очереди других зарегистрированных BeanPostProcessor'ов.
/*
 * Copyright 2002-2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.core;

/**
 * Interface that can be implemented by objects that should be
 * orderable, for example in a Collection.
 *
 * The actual order can be interpreted as prioritization, with
 * the first object (with the lowest order value) having the highest
 * priority.
 *
 * Note that there is a 'priority' marker for this interface:
 * {@link PriorityOrdered}. Order values expressed by PriorityOrdered
 * objects always apply before order values of 'plain' Ordered values.
 *
 * @author Juergen Hoeller
 * @since 07.04.2003
 * @see OrderComparator
 * @see org.springframework.core.annotation.Order
 */
public interface Ordered {

    /**
     * Useful constant for the highest precedence value.
     * @see java.lang.Integer#MIN_VALUE
     */
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;

    /**
     * Useful constant for the lowest precedence value.
     * @see java.lang.Integer#MAX_VALUE
     */
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

    /**
     * Return the order value of this object, with a
     * higher value meaning greater in terms of sorting.
     * Normally starting with 0, with <code>Integer.MAX_VALUE</code>
     * indicating the greatest value. Same order values will result
     * in arbitrary positions for the affected objects.
     * Higher values can be interpreted as lower priority. As a
     * consequence, the object with the lowest value has highest priority
     * (somewhat analogous to Servlet "load-on-startup" values).
     * @return the order value
     */
    int getOrder();

}
Хорошо, это решает наш вопрос с оборачиванием бинов, но вовсе не решает другую часть задачи - "не лезть в целях тестов в production код". Как быть? Будем хачить... Я узнал, что есть некто BeanFactory - реализации которого участвуют в построении бинов. Один из его наследников ConfigurableBeanFactory содержит метод addBeanPostProcessor, с помощью которого можно добавить в систему "ручками" свой BeanPostProcessor. Вот его описание
    /**
     * Add a new BeanPostProcessor that will get applied to beans created
     * by this factory. To be invoked during factory configuration.
     * Note: Post-processors submitted here will be applied in the order of
     * registration; any ordering semantics expressed through implementing the
     * {@link org.springframework.core.Ordered} interface will be ignored. Note
     * that autodetected post-processors (e.g. as beans in an ApplicationContext)
     * will always be applied after programmatically registered ones.
     * @param beanPostProcessor the post-processor to register
     */
    void addBeanPostProcessor(BeanPostProcessor beanPostProcessor);
Теперь мне нужно место, в котором создается BeanFactory чтобы сразу после ее инициализации я добавил бы свой BeanPostProcessor в ее нутро. Где? Дядюшка Дебаг поможет! В итоге я пришел к самому главному XmlWebApplicationContext, который содержит в себе метод createBeanFactory. Занаследовавшись от него я получаю возможность реализовать желаемое
public class SpyXmlWebApplicationContext extends XmlWebApplicationContext {

    @Override
    protected DefaultListableBeanFactory createBeanFactory() {
        DefaultListableBeanFactory factory = super.createBeanFactory();
        factory.addBeanPostProcessor(new SpyPostProcessor());
        return factory;
    }
}
Мы уже близко к цели - теперь осталось поменять класс контекста в web.xml с XmlWebApplicationContext на мой SpyXmlWebApplicationContext.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">
    <display-name>Mocker</display-name>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>intergation.SpyXmlWebApplicationContext</param-value> 
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:com/applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
...
Но мы жеж договорились, что никаких грязных рук в production коде. А потому идем к нашему jetty и будем просить его нам помочь. Если мы добавим ServletContextListener лиснер для загруженного WebAppContext, то во время его обработки можно будет изменить все эти context-param свойства на значения, которые нам нужны.
package integraion;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class JettyRunner {
    public static int start() throws Exception {
        Server server = new Server(0);
        final WebAppContext context = new WebAppContext("src/main/webapp", "/mocker");
        context.addEventListener(new ServletContextListener() {
            @Override
            public void contextInitialized(ServletContextEvent sce) {
                String contextClass = context.getInitParameter(ContextLoader.CONTEXT_CLASS_PARAM);
                if (!contextClass.equals(XmlWebApplicationContext.class.getName())) {
                    throw new RuntimeException("Тип " + contextClass  + " не поддерживается!"); // да-да :)
                }
                context.setInitParameter(ContextLoader.CONTEXT_CLASS_PARAM, SpyXmlWebApplicationContext.class.getName()); // заменяем на свой
            }
        });
        server.setHandler(context);
        server.start();
        int port = server.getConnectors()[0].getLocalPort();
        return port;
    }
}
Все, паззл собрался. Теперь осталось добавить возможности конфигурировать это все дело - потому как у нас если помнишь имя бина захардкоджено. Но это уже не так важно - тем более, что в этих исходниках, я как бы все уже причесал. Качай на зоровье!

среда, 23 апреля 2014 г.

Как я кодю - проект PhotoSlider [Web/Java/Maven/Jetty/Spring/WebDriver]

В прошлом много всяких кодинг-отчетов писал. Пользовался при этом скриншотами и текстом. Но хочется пойти дальше. Было несколько попыток снимать видео того, что происходит на экране пока я кодю - судя по отзывам весьма успешные.

Ценность какая в этом всем? Я бы с удовольствием по-подглядывал как кто-то делает свою работу. Уверен, что есть ребята, которым это так же интересно. А так как правило "будь источником изменений, которые хочешь увидеть" работает, то начну (или продолжу) с себя.

Я люблю много фоткать, особенно, когда какие-то вылазки за город по абсолютно рендомному маршруту. Так вот самая боль - это то, что массу фоток потом хочется описать и выложить в блоге. Но это время. Да, можно выложить все фотки на пикаске и под каждой откомментить что-либо. Но это по моему не то - хочу, чтобы отчет был у меня в том месте, где я скажу, а не где-то там на гугле. Потому приходится выравнивать фоточки в HTML редакторе блога, миксовать их с текстом, перечитывать, поправлять. Это рутина. Ее либо автоматизировать стоит, либо ускорить. Вот потому я и начал писать свой проектик. Кроме того свой pet-проектик всегда можно расширить и дополнить теми фичами, которые хочется именно тебе, а не искать/ждать выхода плагинов на какой-то платформе (тот же гугл/блогспот). Да и развиваю навыки кодописательства с каждым таким домашним проектиком. И конечно же могу пошарить все, что узнал сам на страничках в блоге. Пусть эти посты кого-то вдохновят на личные подвиги.

Итак один рывок вчера сделал - проект уже можно задемить. Есть 4 часа видео, которое конечно же raw не стоит выкладывать (я жеж говорил, что хочу совершенстоваться), а потому я буду выкладывать его частями после монтажа. Почему монтажом заниматься важно? Потому что в ходе монтажа ты получаешь обратную связь о том, как стоило писать видео - и это повлияет на качеству будущих записей. Уверен когда-нибудь я смогу выкладывать raw и без монтажа - и он будет классный. А пока так. 

И да, если я сегодня что-нибудь выложу - потом к этому более вероятнее вернусь.

Продолжение обязательно следует. Отпиши пожалуйста +1 в коммент, если это интересно - подлей масла в огонь :).

пятница, 28 марта 2014 г.

Простой возвращатель html файлов

Задачка была сделать так, чтобы по урлу http://127.0.0.1:8080/context вытаскивалось содержимое папки context корня проекта (html/css/js). Причем если я соберу war И задеплою на сервак, то корнем проекта будет считаться корневая папка вебконтейнера. Если запрашиваемого файла нет, создаю новый.

Велосипед короче очередной. Но надо было быстро.

Вот архив.

Вот основной сервлет

package com.apofig;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;

public class SampleServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        try {

            String context = req.getContextPath();
            if (context.startsWith("/")) context = context.substring(1);

            String path = req.getServletPath();
            if (path.equals("/") || path.equals("")) path = "/index.html";

            String fileName = context + path;
            File file = new File(fileName);
            if (!file.exists()) {
                file.createNewFile();
            }

            String mimeType = getServletContext().getMimeType(file.getPath());
            resp.setContentType(mimeType);
            resp.setContentLength((int) file.length());

            if (mimeType == null) {
                resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                return;
            }

            write(resp, file);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // thanks to http://stackoverflow.com/questions/8623709/output-an-image-file-from-a-servlet
    private void write(HttpServletResponse resp, File file) {
        try {
            OutputStream out = null;
            FileInputStream in = null;
            try {
                in = new FileInputStream(file);
                out = resp.getOutputStream();

                // Copy the contents of the file to the output stream
                byte[] buf = new byte[1024];
                int count = 0;
                while ((count = in.read(buf)) >= 0) {
                    out.write(buf, 0, count);
                }
            } finally {
                if (out != null) out.close();
                if (in != null) in.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }
}

Вот web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">
    <display-name>WebSocketServer</display-name>

    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>com.apofig.SampleServlet</servlet-class>
        <load-on-startup>100</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

И pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>WebServer</groupId>
    <artifactId>WebServer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <jetty.version>8.1.10.v20130312</jetty.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>${jetty.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-client</artifactId>
            <version>${jetty.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>${jetty.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-jsp</artifactId>
            <version>${jetty.version}</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <filtering>true</filtering>
                <excludes>
                    <exclude>**/*.java</exclude>
                </excludes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>${jetty.version}</version>
                <configuration>
                    <webApp>
                        <contextPath>/context</contextPath>
                    </webApp>
                    <stopPort>9999</stopPort>
                    <stopKey>foo</stopKey>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <encoding>utf8</encoding>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-jspc-maven-plugin</artifactId>
                <version>${jetty.version}</version>
                <executions>
                    <execution>
                        <id>jspc</id>
                        <goals>
                            <goal>jspc</goal>
                        </goals>
                        <configuration>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <webXml>${basedir}/target/web.xml</webXml>
                </configuration>
            </plugin>

        </plugins>
    </build>

</project>

Проект собирается
     mvn clean
     mvn package
     mvn war:war
Стартовать его можно
     mvn jetty:run

Может пригодится кому

четверг, 13 марта 2014 г.

Maven Jetty WebSocket server sample

Пусть тут пока полежит... Качаем

Кстати это то, чего практически не делают на просторах сети. Ну разве так сложно выложить вместе с кусками кода в блоге еще и архив с проектом? Намерен эту ошибку интернета исправлять примером.

Что для этого надо?

Сердце любой веб-аппки. Говорим, что будет один сервлет, он будет отвечать на запросы типа http://127.0.0.1:8080/ws

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">
    <display-name<WebSocketServer</display-name>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>

    <servlet>
        <servlet-name>wsServlet</servlet-name>
        <servlet-class>com.apofig.SampleWebSocketServlet</servlet-class>
        <load-on-startup>100</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>wsServlet</servlet-name>
        <url-pattern>/ws/*</url-pattern>
    </servlet-mapping>
</web-app>

Сервлет обработчик

package com.apofig;

import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;

import javax.servlet.http.HttpServletRequest;

public class SampleWebSocketServlet extends WebSocketServlet {

    @Override
    public WebSocket doWebSocketConnect(HttpServletRequest httpRequest, String protocol) {
        final String id = httpRequest.getParameter("id");
        return new WebSocket.OnTextMessage() {

            @Override
            public void onMessage(String data) {
                System.out.println(id + " message > " + data);
            }

            @Override
            public void onOpen(WebSocket.Connection connection) {
                System.out.println(id + " open");
            }

            @Override
            public void onClose(int closeCode, String message) {
                System.out.println(id + " close");
            }
        };
    }
}

Welcome page для тестирования подключения
<!DOCTYPE html>
<meta http-equiv="Content-Type" content="text/html;charset=UTF8">
<html>
<head>
    <title>Websocket Sample</title>
    <script>
        // Спасибо http://learn.javascript.ru/websockets
        var id = Math.floor(Math.random()*10000);
        var socket = new WebSocket("ws://127.0.0.1:8080/ws?id=" + id);
        socket.onopen = function() {
            alert("Соединение установлено для id=" + id);

            var data = "some data";
            socket.send(data);
            alert("Отправлены данные '" + data + "'");
        };

        socket.onclose = function(event) {
            if (event.wasClean) {
                alert('Соединение закрыто чисто');
            } else {
                alert('Обрыв соединения'); // например, "убит" процесс сервера
            }
            alert('Код: ' + event.code + ' причина: ' + event.reason);
        };

        socket.onmessage = function(event) {
            alert("Получены данные " + event.data);
        };

        socket.onerror = function(error) {
            alert("Ошибка " + error.message);
        };
    </script>
</head>
<body>

</body>
</html>

Main класс - раннер
package com.apofig;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

public class Main {

    public static void main(String[] args) throws Exception{
        String webappDirLocation = "src/main/webapp/";

        String webPort = System.getenv("PORT");
        if(webPort == null || webPort.isEmpty()) {
            webPort = "8080";
        }

        Server server = new Server(Integer.valueOf(webPort));
        WebAppContext root = new WebAppContext();

        root.setContextPath("/");
        root.setDescriptor(webappDirLocation+"/WEB-INF/web.xml");
        root.setResourceBase(webappDirLocation);
        root.setParentLoaderPriority(true);

        server.setHandler(root);
        server.start();
        server.join();
    }
}

Конечно же это все можно запустить плагином maven, а потому вот pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>WebSocketServer</groupId>
    <artifactId>WebSocketServer</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <jetty.version>8.1.10.v20130312</jetty.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>${jetty.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-client</artifactId>
            <version>${jetty.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>${jetty.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-jsp</artifactId>
            <version>${jetty.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-websocket</artifactId>
            <version>${jetty.version}</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <filtering>true</filtering>
                <excludes>
                    <exclude>**/*.java</exclude>
                </excludes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>${jetty.version}</version>
                <configuration>
                    <webApp>
                        <contextPath>/</contextPath>
                    </webApp>
                    <stopPort>9999</stopPort>
                    <stopKey>foo</stopKey>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <encoding>utf8</encoding>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-jspc-maven-plugin</artifactId>
                <version>${jetty.version}</version>
                <executions>
                    <execution>
                        <id>jspc</id>
                        <goals>
                            <goal>jspc</goal>
                        </goals>
                        <configuration>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <webXml>${basedir}/target/web.xml</webXml>
                </configuration>
            </plugin>

        </plugins>
    </build>
</project>

Уверен пригодится...

среда, 16 октября 2013 г.

Шаблончик Spring MVC приложения с WebDriver в качестве тест фреймворка + Maven + Jetty

Вот недавно кодили с другом на тему ТДД веб приложение и конечно же первым делом, мы на час зависли на самом старте - начинать с нуля веб приложения на Java не так чтобы сложно, но муторно - с другими площадками попроще будет.

Так что предлагаю простой шаблончик Spring MVC приложения с WebDriver в качестве тест фреймворка и Maven в качестве резолвера зависимостей, который ранится на Jetty.


Все что он умеет делать - добавлять тудушки в список. Опять же - цели  написать полноценный TODO не стояло. Цель помочь тем, кто не знает с чего начать и хочет начать писать веб приложения на java. Этого думаю достаточно. Вот исходники. Чтобы не пропали.

Очень рекомендую IDEA - в нее стоит импортировать проект как Maven. запустить все тесты - они должны пройти. После целью jetty:run можно запустить приложение.


Дальше уже сами...

воскресенье, 18 ноября 2012 г.

Как мокать Bean'ы внутри Spring context запущеного Jetty?

Есть Web приложение на Spring (MVC + IoC). Есть Jetty сервер, на котором оно ранится. Web приложение в своей Model содержит ряд сервисов (Spring Beans), которыми пользуются контроллера.

Задача: Написать функциональный тест с испольованием WebDriver, но так, чтобы из теста была возможность мокать реальные сервиса модели.

Updated: Есть более продвинутая имплементация этой задачи, вот тут. Все что дальше по тексту этого поста - на память :)

Например в приложении есть 1 сервис MyService
package com.services;

import org.springframework.stereotype.Component;

/**
 * User: apofig
 * Date: 11/18/12
 * Time: 9:49 PM
 */
@Component("myService")
public class MyService {

    public String getString() {
        return "Hello from " + MyService.class.getSimpleName() + "!";
    }

}
Он инджектится в контроллер MyController с помощью @Autowired аннотации Spring'ом.
package com.controller;

import com.services.MyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * User: apofig
 * Date: 9/20/12
 * Time: 1:37 PM
 */
@Controller
@RequestMapping("/")
public class MyController {

    @Autowired
    private MyService service;

    @RequestMapping(method = RequestMethod.GET)
    public String savePlayerGame(Model model) {
        model.addAttribute("string", service.getString());
        return "view";
    }
}
Контроллер отправляет данные на jsp, которая рисует сообщение на экране
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;">
    <title>Main page</title>
</head>
<body>
    <c:if test="${string!=null}">
        <span id="message">${string}</span>
    </c:if>
<body>
</html>
Во время старта приложения я хочу, чтобы там был не реальный MyService, а его мок (или шпион), которым я буду рулить из теста вот так
public class IntegrationTest {
    private String url = "http://localhost:8080/";
    private WebDriver driver = new HtmlUnitDriver(true);
    private MyService service;

    @Test
    public void shouldGetMessageFromService() throws IOException, InterruptedException {
        when(service.getString()).thenReturn("Hi from mock!");

        driver.get(url);

        String message = driver.findElement(By.id("message")).getText();
        assertEquals("Hi from mock!", message);
    }
}
Зачем это делать? Если писать полностью функиональные тесты, тогда чтобы протестить, скажем, удаление пользователя из системы надо будет вначале создать этого пользователя по всем правилам. Но если иметь возможность мокать модель, тогда лишние действия не потребуются - мы всего лишь проверим, что до модели дошел запрос о удалении юзера и все.

Итак я долго (дня два) искал возможность это сделать. Google почти молчит. Самое близкое, что я нашел - библиотека Springockito, но нашел я ее тогда, когда практически моя версия была рабочей.

Как я решал проблему? При старте Jetty сервера есть возможность подслушать момент инициплизации контекста.
    Server server = new Server(0);
    WebAppContext context = new WebAppContext("src/main/webapp", "");
    context.addEventListener(new ServletContextListener() {
        @Override
        public void contextInitialized(ServletContextEvent sce)
            ServletContext servletContext = sce.getServletContext(); 
            // вот тут       
        }
    }
    server.setHandler(context);
    server.start();
    int port = server.getConnectors()[0].getLocalPort();
    String url = "http://localhost:" + port;
Я так же знаю, что после окончательного старта сервера в ServletContext будет находиться Spring'овый WebApplicationContext, но в момент отработки ServletContextListener его там еще нет.

После старта сервера я могу его получить вот так
    WebApplicationContext applicationContext = 
        WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
Имея на руках Spring'овый контекст я могу поковырять его - могу удалить существующий бин и заменить его моком, так что всякий раз когда клиент будет делать context.getBean(name) он будет получать мой мок. Вперед!
    private static Object mocking(WebApplicationContext webAppContext, String beanName, boolean isMockOfSpy) {
        Object realBean = webAppContext.getBean(beanName);

        AbstractRefreshableApplicationContext context = (AbstractRefreshableApplicationContext) webAppContext;
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();

        Object newBean = null;
        if (isMockOfSpy) {
            newBean = mock(realBean.getClass());
        } else {
            newBean = spy(realBean);
        }

        beanFactory.removeBeanDefinition(beanName);
        beanFactory.registerSingleton(beanName, newBean);
        return beanFactory.getBean(beanName);
    }
Конечно, тут не помешает пару проверок (для ClassCast), потому как контекст мог быть не обязательно XmlWebApplicationContext как у меня - а там иди знай, что вернется.

Но это не все. В момент, когда я могу провернуть такую махинацию Spring уже проинджектил все бины в поля зависимых классов по аннотации @Autowired. И хоть я могу получить мок с помощью context.getBean(name), меня это не устраивает.

Несколько часов дебага с кондишенал брейкопинтами и я поймал момент, когда WebApplicationContext уже имеет на руках все бины, но еще не успел расставить их куда велено. Любопытно, что этот момент - такой же как и мой обработчик ServletContextListener.

В момент когда я добавляю свой обработчик, список обработчиков WebAppContext пуст (т.е. я первым слушаю) - тут нигде нет никакого WebApplicationContext. Даже в момент, когда мой обработчик отрабатывает, WebApplicationContext'а так же нигде нет.
    Server server = new Server(0);
    WebAppContext context = new WebAppContext("src/main/webapp", "");
    // Spring контекст недуступен
    context.addEventListener(new ServletContextListener() {
        @Override
        public void contextInitialized(ServletContextEvent sce)
            ServletContext servletContext = sce.getServletContext();        
            // Spring контекст так же недуступен
        }
    }
    server.setHandler(context);
    server.start();
    int port = server.getConnectors()[0].getLocalPort();
    String url = "http://localhost:" + port;
Обыно он хранится в _contextAttributes, но тут пока пусто
Но я точно знаю, что он там будет спустя некоторое время (ибо додебажился).
Через некоторое время, в очередь, после моего ServletContextListener обработчика, выстрится ряд других обработчиков. Последним из них будет Spring'овый. И вот когда он отработает, то WebApplicationContext появится внутри ServletContext.В этот момент бины будут уже готовые, но еще пока не расставленные по зависимым классам.

Мне надо как-то стать пятым в очереди, и сделал я это так
    private ServletContext servletContext;
    private WebApplicationContext applicationContext;

    public String start() throws Exception {
        server = new Server(0);
        final WebAppContext context = loadWebContext();
        context.addEventListener(new ServletContextListener() {
            @Override
            public void contextInitialized(ServletContextEvent sce) {
                servletContext = sce.getServletContext();

                context.addEventListener(new ServletContextListener() {
                    @Override
                    public void contextInitialized(ServletContextEvent sce) {
                        applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
                        // тут бины тепленькие, и все мои!!!
                    }

                    @Override
                    public void contextDestroyed(ServletContextEvent sce) {
                    }
                });
            }

            @Override
            public void contextDestroyed(ServletContextEvent sce) {
            }
        });
        server.setHandler(context);
        server.start();
        int port = server.getConnectors()[0].getLocalPort();

        String url = "http://localhost:" + port;
        return url;
    }
Дальше дело техники, вызываем метод
    private static Object mocking(WebApplicationContext webAppContext, String beanName, boolean isMockOfSpy) {
И сразу после того, как отработчик отработает Spring полезет деливерить бины по зависимым клиентам (@Autowired).

Я в дамках!

Вот исходный код простенького Spring Web проекта с демонстрацией этой возможности.