Добрый день всем хаброжителям!

Поводом к написанию статьи послужило, то что к моему большому удивлению на хабре я не нашёл статьи о реализации RESTful Web Service на Java, может, конечно, плохо искал. Да написано про RESTful web services очень много, но как то вот так, чтобы простенько с примерами кода, рабочий сервис, не так уж и легко найти и не только на хабре…

Вообще с REST я познакомился совсем недавно, не больше месяца назад. Так что буду очень благодарен за советы, поправки и критику!

Разобраться было и так вообщем то не сложно, но я думаю аналогичный пост мне бы очень помог и сильно бы ускорил процесс обучения! Тем более, если вы начинающий разработчик и о многом только слышали, а руками никогда не трогали.

По моему первому впечатлению: действительно вещь очень удобная, а главное очень простая, ещё и если использовать JSON, а не XML, ну по крайней мере мне так показалось после опыта работы с SOAP и WSDL. Ну, да об этом я думаю и так все знают, кто хоть немного работал с веб сервисами.

Так что, кто заинтересовался реализацией, прошу под кат

Сразу оговоримся:

1. весь код, конечно, в статье не выложишь и о нём не расскажешь;
2. версия проектика, конечно, не финальная и, как я уже говорил выше — буду очень благодарен за замечания и советы;
3. конечно же, есть баги.

И так, начнём по порядку:

1. Используемое ПО:


ПО выбиралось по очень простому принципу — чем проще, тем лучше. Да, вместо MySQL для нагруженных сервисов без необходимости делать сложные запросы в базу, очень хорошо использовать MongoDB, ну, по крайней мере по этому поводу много написанно, да и опять же удобнее её использовать так как на входе тот же JSON.

2. В принципе что будет делать наш сервис — тут всё очень банально: сервис будет работать с одной табличкой в БД — сосбственно вставлять, апдейтать, удалять, ну и, конечно же, получать записи списком или по Id. Конечно же, хотелось бы иметь возможность параметризированного запроса на получение списка записей, не плох�� было бы сделать «красивый» урл к сервису, прикрутить какой-нибудь интерсептор, чтобы, например, проверять права пользователя на доступ к сервису, или что-нибудь другое делать перед запуском сервиса, ну и как-то централизованно управлять кодами ошибок в ответах от сервера.

Cобственно, табличка:

CREATE TABLE `customer` (
  `id` varchar(45) NOT NULL,
  `first_name` varchar(45) DEFAULT NULL,
  `last_name` varchar(45) DEFAULT NULL,
  `phone` varchar(45) DEFAULT NULL,
  `mail` varchar(45) DEFAULT NULL,
  `adress` varchar(45) DEFAULT NULL,
  `contract_id` varchar(45) DEFAULT NULL,
  `contract_expire_date` date DEFAULT NULL
) 


WS Endpoints:

1. http://mysite.com/service/customer

2. http://mysite.com/service/customer/{id}


4 стандартных статуса, которые мы будем дополнительно обрабатывать (например, добавлять версию наших веб сервисов в ответ и при ошибке — наш код ошибки):

200 — Successful;
401 — Not Authorized;
404 — Not Found;
500 — Server error during operation.

3. Реализация (код на гитхабе тут):

Да, код, минимально комментировал, описание аннотаций тут.

Сами веб сервисы:
public class CustomersServiceJSON implements ICustomersService {

	// link to our dao object
	private ICustomersDAO customersDAO;

	// for customersDAO bean property injection
	public ICustomersDAO getCustomersDAO() {
		return customersDAO;
	}

	public void setCustomersDAO(ICustomersDAO customersDAO) {
		this.customersDAO = customersDAO;
	}

	// for retrieving request headers from context
	// an injectable interface that provides access to HTTP header information.
	@Context
	private HttpHeaders requestHeaders;

	private String getHeaderVersion() {
		return requestHeaders.getRequestHeader("version").get(0);
	}

	// get by id service
	@GET
	@Path("/{id}")
	public Response getCustomer(@PathParam("id") String id) {
		Customer customer = customersDAO.getCustomer(id);
		if (customer != null) {
			return ResponseCreator.success(getHeaderVersion(), customer);
		} else {
			return ResponseCreator.error(404, Error.NOT_FOUND.getCode(),
					getHeaderVersion());
		}
	}

	// remove row from the customers table according with passed id and returned
	// status message in body
	@DELETE
	@Path("/{id}")
	public Response removeCustomer(@PathParam("id") String id) {
		if (customersDAO.removeCustomer(id)) {
			return ResponseCreator.success(getHeaderVersion(), "removed");
		} else {
			return ResponseCreator.success(getHeaderVersion(), "no such id");
		}
	}

	// create row representing customer and returns created customer as
	// object->JSON structure
	@POST
	@Consumes(MediaType.APPLICATION_JSON)
	public Response createCustomer(Customer customer) {
		System.out.println("POST");
		Customer creCustomer = customersDAO.createCustomer(customer);
		if (creCustomer != null) {
			return ResponseCreator.success(getHeaderVersion(), creCustomer);
		} else {
			return ResponseCreator.error(500, Error.SERVER_ERROR.getCode(),
					getHeaderVersion());
		}
	}

	// update row and return previous version of row representing customer as
	// object->JSON structure
	@PUT
	@Consumes(MediaType.APPLICATION_JSON)
	public Response updateCustomer(Customer customer) {
		Customer updCustomer = customersDAO.updateCustomer(customer);
		if (updCustomer != null) {
			return ResponseCreator.success(getHeaderVersion(), updCustomer);
		} else {
			return ResponseCreator.error(500, Error.SERVER_ERROR.getCode(),
					getHeaderVersion());
		}
	}

	// returns list of customers meeting query params
	@GET
	//@Produces(MediaType.APPLICATION_JSON)
	public Response getCustomers(@QueryParam("keyword") String keyword,
			@QueryParam("orderby") String orderBy,
			@QueryParam("order") String order,
			@QueryParam("pagenum") Integer pageNum,
			@QueryParam("pagesize") Integer pageSize) {
		CustomerListParameters parameters = new CustomerListParameters();
		parameters.setKeyword(keyword);
		parameters.setPageNum(pageNum);
		parameters.setPageSize(pageSize);
		parameters.setOrderBy(orderBy);
		parameters.setOrder(Order.fromString(order));
		List<Customer> listCust = customersDAO.getCustomersList(parameters);
		if (listCust != null) {
			GenericEntity<List<Customer>> entity = new GenericEntity<List<Customer>>(
					listCust) {
			};
			return ResponseCreator.success(getHeaderVersion(), entity);
		} else {
			return ResponseCreator.error(404, Error.NOT_FOUND.getCode(),
					getHeaderVersion());
		}
	}
}


Всё достаточно просто — 4 веб сервиса в зависимости от URI и метода которым этот URI дёргается, есть объект DAO, который подключается в beans.xml и доступ к заголовкам запроса, чтобы доставать для примера кастомный заголовок «version».

Штука, которая отрабатывает перед тем как вызывается сервис:
public class PreInvokeHandler implements RequestHandler {
	
	// just for test
	int count = 0;	

	private boolean validate(String ss_id) {
		// just for test
		// needs to implement		
		count++;
		System.out.println("SessionID: " + ss_id);
		if (count == 1) {
			return false;
		} else {
			return true;
		}
	}

	public Response handleRequest(Message message, ClassResourceInfo arg1) {
		Map<String, List<String>> headers = CastUtils.cast((Map<?, ?>) message
				.get(Message.PROTOCOL_HEADERS));
		
		if (headers.get("ss_id") != null && validate(headers.get("ss_id").get(0))) {
			// let request to continue
			return null;
		} else {
			// authentication failed, request the authentication, add the realm			
			return ResponseCreator.error(401, Error.NOT_AUTHORIZED.getCode(), headers.get("version").get(0));
		}	
	
	}
}


Здесь в методе validate() можно проверять какие-то пред условия, чисто для теста добавлена проверка кастомного заголовка в запросе идентификатор сессии «ss_id», ну, и с первого раза даже с этим заголовком будет падать 401.

Общий обработчик exceptions:
public class CustomExceptionMapper implements ExceptionMapper<Exception> {
	@Context
	private HttpHeaders requestHeaders;	
	
	private String getHeaderVersion() {		
		return requestHeaders.getRequestHeader("version").get(0);
	}
	
    public Response toResponse(Exception ex) {    	
    	System.out.println(ex.getMessage() + ex.getCause());
        return ResponseCreator.error(500, Error.SERVER_ERROR.getCode(), getHeaderVersion());
    }
}


Что-то уже многовато кода для поста, есть ещё вспомогательный класс для формирования ответа серверу и глобальный enum для хранения наших кодов ошибок. Да, дескриптор развёртывания и beans.xml всё таки приведу тут:

web.xml:
...
<web-app>
	<display-name>service</display-name>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/beans.xml</param-value>
    </context-param>
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
    <servlet>
        <servlet-name>CXFServlet</servlet-name>
        <display-name>CXF Servlet</display-name>
        <servlet-class>
            org.apache.cxf.transport.servlet.CXFServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>CXFServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>


Тут основной интерес представляет подключение экшн сервлета от Apache — CXFServlet и стандартного спрингового ContextLoaderListener.

beans.xml:
...
<!-- Imported resources for cxf -->
	<import resource="classpath:META-INF/cxf/cxf.xml" />
	<import resource="classpath:META-INF/cxf/cxf-extension-jaxrs-binding.xml" />
	<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
	
	<!-- Imported bean for dao -->
	<import resource="classpath:META-INF/spring/dao.xml"/>

	<bean id="customersService" class="com.test.services.customers.rest.CustomersServiceJSON">
		<property name="customersDAO" ref="customersDAO"/>
	</bean>

	<bean id="preInvokeHandler" class="com.test.services.rest.PreInvokeHandler" />
	<bean id="customExceptionMapper" class="com.test.services.rest.CustomExceptionMapper" />

	<jaxrs:server id="restContainer" address="/customer">
		<jaxrs:serviceBeans>
			<ref bean="customersService" />
		</jaxrs:serviceBeans>
		<jaxrs:providers>
			<ref bean="preInvokeHandler" />
			<ref bean="customExceptionMapper" />
		</jaxrs:providers>
	</jaxrs:server>
.........


Здесь, собственно, задали нужные конфигурационные файлики для CXF, подключили DAO объект, наш предобработчик и обработчик исключительных ситуаций, конечно же, сам бин с сервисами и задали корень для сервисов.

Для того чтобы подёргать сервисы я использовал REST Console 4.0.2 плагин для хрома — штука достаточно простая, главное задать нужные ендпоинт, кастомные заголовки (как я уже говорил без «ss_id» всегда будет падать 401) и контент тип. Для примера:

Request Body:
Request Url: http://localhost:8080/service/customer
Request Method: GET
Status Code: 200

Request headers:
Accept: application/json
Content-Type: application/json
ss_id: 12312.111
version: 12312.111
........

Response headers:
Status Code: 200
Date: Tue, 21 Aug 2012 13:09:45 GMT
Content-Length: 877
Server: Apache-Coyote/1.1
Content-Type: application/json
version: 12312.111

Response body:
{
    "customer": [{
        "id": "89ad5a46-c9a2-493f-a583-d8250ee31766",
        "adress": "null",
        "contract_id": "null",
        "first_name": "serg",
        "last_name": "serg",
        "mail": "serg",
        "phone": "null"
    }, {
        "id": "300ff688-a783-4e6a-9048-8bb625128dc0",
        "first_name": "serg"
    }, {
        "id": "67731ab9-87b1-4ff9-a7e4-618c1f9e8c4c",
        "first_name": "serg"
    }, {
        "id": "cd5039bb-031f-4697-a70c-ad3e628963dd",
        "first_name": "serg"
    }, {
        "id": "86da5446-7439-4242-b730-31c8b57a5c7d",
        "first_name": "serg"
    },
..........


И последнее, хотелось иметь «красивый», лучше скажем нужный нам урл к вебсервисам. Кнечно, можно поправить server.xml или использовать какой-нибудь тул для urlRewrite, но по моему самый простой способ это запаковать наш веб архив в ear и задать другой рут для наших веб-сервисов в application.xml, но в рамках данного поста я этого уже делать не буду.

P.S.: Надеюсь, что данный пост будет полезен тем кто хочет познакомиться с Java RESTful web services, а более опытные посоветуют и покритикуют!