Добрый день всем хаброжителям!
Поводом к написанию статьи послужило, то что к моему большому удивлению на хабре я не нашёл статьи о реализации RESTful Web Service на Java, может, конечно, плохо искал. Да написано про RESTful web services очень много, но как то вот так, чтобы простенько с примерами кода, рабочий сервис, не так уж и легко найти и не только на хабре…
Вообще с REST я познакомился совсем недавно, не больше месяца назад. Так что буду очень благодарен за советы, поправки и критику!
Разобраться было и так вообщем то не сложно, но я думаю аналогичный пост мне бы очень помог и сильно бы ускорил процесс обучения! Тем более, если вы начинающий разработчик и о многом только слышали, а руками никогда не трогали.
По моему первому впечатлению: действительно вещь очень удобная, а главное очень простая, ещё и если использовать JSON, а не XML, ну по крайней мере мне так показалось после опыта работы с SOAP и WSDL. Ну, да об этом я думаю и так все знают, кто хоть немного работал с веб сервисами.
Так что, кто заинтересовался реализацией, прошу под кат
Сразу оговоримся:
1. весь код, конечно, в статье не выложишь и о нём не расскажешь;
2. версия проектика, конечно, не финальная и, как я уже говорил выше — буду очень благодарен за замечания и советы;
3. конечно же, есть баги.
И так, начнём по порядку:
1. Используемое ПО:
- JDK 1.6
- Apache_CXF
- Spring 3 Framework JDBC
- Apache Tomcat 7.0
- MySQL 5.1
- Eclipse 4.2 Juno
- Maven 3.0
ПО выбиралось по очень простому принципу — чем проще, тем лучше. Да, вместо 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, а более опытные посоветуют и покритикуют!
Автор: sergeisirik