Добрый день всем хаброжителям!
Поводом к написанию статьи послужило, то что к моему большому удивлению на хабре я не нашёл статьи о реализации RESTful Web Service на Java, может, конечно, плохо искал. Да написано про RESTful web services очень много, но как то вот так, чтобы простенько с примерами кода, рабочий сервис, не так уж и легко найти и не только на хабре…
Вообще с REST я познакомился совсем недавно, не больше месяца назад. Так что буду очень благодарен за советы, поправки и критику!
Разобраться было и так вообщем то не сложно, но я думаю аналогичный пост мне бы очень помог и сильно бы ускорил процесс обучения! Тем более, если вы начинающий разработчик и о многом только слышали, а руками никогда не трогали.
По моему первому впечатлению: действительно вещь очень удобная, а главное очень простая, ещё и если использовать JSON, а не XML, ну по крайней мере мне так показалось после опыта работы с SOAP и WSDL. Ну, да об этом я думаю и так все знают, кто хоть немного работал с веб сервисами.
Так что, кто заинтересовался реализацией, прошу под кат
Сразу оговоримся:
1. весь код, конечно, в статье не выложишь и о нём не расскажешь;
2. версия проектика, конечно, не финальная и, как я уже говорил выше — буду очень благодарен за замечания и советы;
3. конечно же, есть баги.
И так, начнём по порядку:
1. Используемое ПО:
ПО выбиралось по очень простому принципу — чем проще, тем лучше. Да, вместо MySQL для нагруженных сервисов без необходимости делать сложные запросы в базу, очень хорошо использовать MongoDB, ну, по крайней мере по этому поводу много написанно, да и опять же удобнее её использовать так как на входе тот же JSON.
2. В принципе что будет делать наш сервис — тут всё очень банально: сервис будет работать с одной табличкой в БД — сосбственно вставлять, апдейтать, удалять, ну и, конечно же, получать записи списком или по Id. Конечно же, хотелось бы иметь возможность параметризированного запроса на получение списка записей, не плох�� было бы сделать «красивый» урл к сервису, прикрутить какой-нибудь интерсептор, чтобы, например, проверять права пользователя на доступ к сервису, или что-нибудь другое делать перед запуском сервиса, ну и как-то централизованно управлять кодами ошибок в ответах от сервера.
Cобственно, табличка:
WS Endpoints:
4 стандартных статуса, которые мы будем дополнительно обрабатывать (например, добавлять версию наших веб сервисов в ответ и при ошибке — наш код ошибки):
200 — Successful;
401 — Not Authorized;
404 — Not Found;
500 — Server error during operation.
3. Реализация (код на гитхабе тут):
Да, код, минимально комментировал, описание аннотаций тут.
Сами веб сервисы:
Всё достаточно просто — 4 веб сервиса в зависимости от URI и метода которым этот URI дёргается, есть объект DAO, который подключается в beans.xml и доступ к заголовкам запроса, чтобы доставать для примера кастомный заголовок «version».
Штука, которая отрабатывает перед тем как вызывается сервис:
Здесь в методе validate() можно проверять какие-то пред условия, чисто для теста добавлена проверка кастомного заголовка в запросе идентификатор сессии «ss_id», ну, и с первого раза даже с этим заголовком будет падать 401.
Общий обработчик exceptions:
Что-то уже многовато кода для поста, есть ещё вспомогательный класс для формирования ответа серверу и глобальный enum для хранения наших кодов ошибок. Да, дескриптор развёртывания и beans.xml всё таки приведу тут:
web.xml:
Тут основной интерес представляет подключение экшн сервлета от Apache — CXFServlet и стандартного спрингового ContextLoaderListener.
beans.xml:
Здесь, собственно, задали нужные конфигурационные файлики для CXF, подключили DAO объект, наш предобработчик и обработчик исключительных ситуаций, конечно же, сам бин с сервисами и задали корень для сервисов.
Для того чтобы подёргать сервисы я использовал REST Console 4.0.2 плагин для хрома — штука достаточно простая, главное задать нужные ендпоинт, кастомные заголовки (как я уже говорил без «ss_id» всегда будет падать 401) и контент тип. Для примера:
Request Body:
Request headers:
Response headers:
Response body:
И последнее, хотелось иметь «красивый», лучше скажем нужный нам урл к вебсервисам. Кнечно, можно поправить server.xml или использовать какой-нибудь тул для urlRewrite, но по моему самый простой способ это запаковать наш веб архив в ear и задать другой рут для наших веб-сервисов в application.xml, но в рамках данного поста я этого уже делать не буду.
P.S.: Надеюсь, что данный пост будет полезен тем кто хочет познакомиться с Java RESTful web services, а более опытные посоветуют и покритикуют!
Поводом к написанию статьи послужило, то что к моему большому удивлению на хабре я не нашёл статьи о реализации 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, а более опытные посоветуют и покритикуют!
