Реализуем RESTful Web Service на java

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

Поводом к написанию статьи послужило, то что к моему большому удивлению на хабре я не нашёл статьи о реализации 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, а более опытные посоветуют и покритикуют!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 40

    +2
    Хорошая статья, а если ещё и исходники рабочие — то вообще хорошо. Когда мне нужно было работать с xfire и apachecfx везде были одни и те же примеры.
    Но как по мне — spring 3 mvc намного упрощает создание REST вебсервиса.
      0
      Никогда к сожалению не работал со spring 3 mvc, но слышал ни раз мнение более опытных коллег, что привязываться к тому что не задокументировано в JSR не совсем хорошо?
      Да, исходники точно рабочие — собирал поднимал и минимально тестировал.
        0
        Spring это фреймворк. И он следует многим JSR'ам, хочет он того или нет.
          0
          Ну, я к тому просто что все используемые в данном примерчике аннотации описаны в JSR-311.
          А в spring 3 mvc можно ли обойтись, например, только такими же аннотациями или может какими-то из других JSR и будет ли это удобно?
            0
            Я не пробовал JSR аннотации для вебсервисов, т.к. использую спринговские аннотации, но исходя из того что поддерживаются другие JSR (напр. dependency injection, validation) и из того, что существует Spring-WS, то вполне возможно.
            В других случаях для разработчика это очень прозрачно — вместо спринговской аннотации пишется стандартная, которую фреймворк опознает сам.
      +1
      Раз уж вы используете Spring, зачем изобретать свой велосипед? Что такое ResponseCreator? Чем не угодил @ResponseBody? И т.д.
        0
        От спринга использую только JDBCTemplate, в сервисах самих вроде ничего от спринга нету? Со спрингом знаком к сожалению очень отдалённо. И всё тоже к вопросу: @ResponseBody специфицирована в какой-нибудь JSR? Просто тут не нахожу jsr311.java.net/nonav/releases/1.1/spec/spec.html.
        Просто следую совету коллег не использовать фрэймворк специфик штуки.
          0
          Спросите пожалуйста у ваших коллег — к чему такие ограничения?
          Вот к примеру про спринг аннотации — фреймворк задает архитектуру, и даже если использовать JSR аннотации, то из-за детской болезни стандартного Dependency Injection'a нашей любимой Java, вы не сможете просто поменяв либу спринга на либу джавы сделать приложение рабочим.

          Я считаю что ваши коллеги правы в чём-то, к примеру это подошло бы если бы вы использовали в приложение Java Content Repository (JSR-170 вроде), но программирование такая вещь, что нет вечно эффективного рецепта, и всегда нужно думать, подходит ли конкретно эта вещь в конкретно этом месте.
            0
            ну что вы? конечно же надежней запилить свою реализацию, чем пользоваться каким-то там непонятными спрингами :)
              0
              Я понял, что вы предлагаете. Просто я не имея практически опыта работы со Spring не могу высказаться ни за ни против, потому могу основываться только на советах. Поработаю со спрингом, тогда решу, нужен он там мне или нет.
                0
                На простом «попробовать» полное представление о возможностях инструмента сформируются не сразу, а оно должно быть полным чтобы не делать велосипеды. Вот такие толковые новые книги Pro Spring 3 (Apress) (апрель 2012) и Pro Spring MVC — With Web Flow (Apress) (июнь 2012), в принципе достаточно первой там по MVC тоже есть и вообще там рассматриваются все возможности Spring (ну конечно кроме ответвлений Batch, WS, Data и тд).
            0
            >>> не использовать фрэймворк специфик штуки

            Я бы не сказал Spring строго фреймворк, часто его называют легковесным контейнером. Дело в том что вся необходимая инфраструктура заведется на простом любом Java веб сервере, и не нужно никаких серверов приложений (вроде JBOSS, Websphere, OC4J и тд) где поддержка стандартов (JSR этих) реализована в самом сервере и часто выйти за границы «зашитого» (ну хотя бы прикрутить последние версии некоторых стандартов с новыми необходимыми плюшками) в контейнер не просто без обновления (или иногда даже хаков) самого контейнера (в это не всегда возможно, обычно по причине бюрократии так как сервера приложений обычно используют всякие крупные и солидные заказчики), имел опыт. Spring гибок, «зашитого» нету, то есть реализацию и комплектацию контейнера мы выбираем сами, это удобно.
          –2
          В методе getCustomers не очень по «рестовски» использовать @QueryParam
          Лучше:
                  @GET
                  @Produces(MediaType.APPLICATION_JSON)
                  @Path("/customers/{keyword}/{orderby}{pagenum}{pagesize}")
                  public Response getCustomers(@PathParam("keyword") String keyword,
                  @PathParam("orderby") String orderby, @PathParam("pagenum") String pagenum, 
                  @PathParam("pagesize") String pagesize) {
                  	
                  }
          


          Так URL будет красивее

          Ещё можно параметры в URL привязать к regular expression
          Например:
          @Path("/invite/{uid: [0-9]*}")

          Пропустит: example.com/invite/123456
          Не пропустит: example.com/invite/abc12345

            0
            @Path("/customers/{keyword}/{orderby}/{pagenum}/{pagesize}")
            Нечайно слеши пропустил, так правильно.
              0
              а если мне надо просто список кастомеров с сортировкой по фамилии на n-ой странице?
                0
                А чем выше URL не подходит? Или я не понял вопроса… Мне только не ясно что за keyword, его опустим.
                Получается: example.com/customers/lastname/5/10
                Выходит мы хотим получить кастомеров, отсортированных по фамилии начиная с 5 стр, кол-во записей на стр 10
                Тоесть нам надо при запросе в базу пропустить №стр * кол-во записей = 50 записей пропустить и взять 10.
                .skip(50).limit(10)

                Конечно, параметры к нам приходят в виде строки, так что надо конвертнуть в int.
                  0
                  если вычеркнуть keyword совсем, то все просто, да

                  другое дело, что keyword используется, например, для фильтрации по той же фамилии (хотя у sergeisirik в сырцах параметры вообще игнорируются)

                  теперь мой вопрос понятен?
              0
              Да, согласен, так видимо красивее будет. Спасибо, буду использовать такой подход в следующий раз!
                +2
                А почему вы выбрали Apache CXF? Просто мне кажется Jersey намного быстрее развивается. Оба имплементируют jax-rs
                Но Jersey начали пилить ещё в Sun и вот — вот начнут поддерживать JAX-RS 2.0.
                jersey.java.net/
                  0
                  Так уж сложилось, что мой первый опыт работы с jax-rs был именно с Apache CXF. Не слышал просто, что Jersey лучше, нужно будет попробовать.
                    0
                    А в Jersey есть какая-то замена CXF FIQL?
                      0
                      FIQL, как я понял, не относится к CXF — tools.ietf.org/html/draft-nottingham-atompub-fiql-00. Поддержки FIQL в Jersey я по-беглому не нашёл. Вероятно, она не прописана в JSR. Почитав доку на апаче, я увидел, что это стык JAX-RS и JPA/JDBC, так что имхо нужен отдельныq JSR. Вообще штука интересная по части универсализации запросов.
                0
                Автор, горячо советую обратить свой взор на Play! Framework. Вот там REST из коробки и ноль мучений.
                  0
                  Спасибо за совет! Слышал про такой фреймворк, но пока ещё не использовал, знаю людей, которые его используют, но почему то только для UI? а внизу всё так же используют Spring СXF…
                • UFO just landed and posted this here
                    0
                    Я может чего то не понимаю, но вот:
                    The JdbcTemplate class is the central class in the JDBC core package. It handles the creation and release of resources, which helps you avoid common errors such as forgetting to close the connection. It performs the basic tasks of the core JDBC workflow such as statement creation and execution, leaving application code to provide SQL and extract results.
                    Отсюда: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/jdbc.html
                    • UFO just landed and posted this here
                        0
                        В дао используются классы JdbcTemplate и сходные, сам коннект и сессии обслуживается спрингом.
                        • UFO just landed and posted this here
                            0
                            Если дать ему пропроксировать сервисы — сообразит…
                            • UFO just landed and posted this here
                    0
                    Что-то многовато букв в коде для такой задачи.
                      0
                      По мне так если используется Spring, то можно бы взять от него побольше, то есть реализовать REST на Spring MVC, интеграция в спринговый контейнер более тесная (родная) и вообще удобно. А еще есть вот такая новая штука Spring Data — REST, сам еще не пробовал.
                        0
                        Вообще Spring Data довольно интересный проект, этого спрингу давно не хватало. Spring Data JPA пробовал, впечатления положительные.
                          0
                          Насчёт Spring и Hibernate — есть мнение, что лучше не использовать реализации стандартов, вышедшие до появления самих этих стандартов, а то там буду всякие невозможности мигрировать, vendor lock-up и т.д… Spring JDBC ни к каким особым стандартам никаким боком не касается, так что этого принципа не нарушает.
                            0
                            Не совсем понятно почему комментарий адресован мне, но ответить попробую :) В последние годы Hibernate обычно напрямую не используется, а подключается как реализация JSR 220 или просто JPA, то есть стандарт ORM программирования в Java. При таком подходе в принципе Hibernate можно заменить другой реализацией ORM в случае необходимости.
                            • UFO just landed and posted this here
                                0
                                Конечно же, JPA второй ревизии :) Лично я первой не пользовался и вообще не знал тогда об этом.
                                0
                                Я про Spring, насчёт «взять от него побольше»…
                            0
                            Про CXF столкнулся тут с интересной особенностью. Может кому то будет интересно: при объявлении нескольких классов сервисов в beans.xml, по умолчанию поиск нужного метода происходит только в первом по списку, если у вас более одного то надо рализовывать ResourceComparator: http://cxf.apache.org/docs/jax-rs-basics.html#JAX-RSBasics-Customselectionbetweenmultipleresources

                            Only users with full accounts can post comments. Log in, please.