Создаем RESTful web service на Java с использованием Eclipse + Jersey + Glassfish3

в 13:29, , рубрики: eclipse, java, RESTful, Веб-разработка, Программирование, метки: , , ,

Добрый вечер всем!

Не так давно (в феврале этого года), я решил заняться программированием. В качестве языка был выбран Java. И с того момента я упорно изучаю все возможности данного языка. Недавно наткнулся на статью про RESTful на хабре. Прочитал и понял, что надо осветить альтернативный способ создания данных сервисов. Ещё меня поразило, насколько некоторые статьи пишутся непонятно для новичков. Я решил написать статью в которой расскажу и о прикладной части создания веб сервиса.

Я не претендую на истину. Я просто хочу показать простой и быстрый способ создать веб сервис.

Начало

Необходимое ПО

Самое главное это IDE. Я предпочитаю Eclipse. Данный проект я написал в Eclipse for Java EE Developers Juno. Четно говоря, предыдущий релиз был стабильнее, но этот внешне приятнее. В качестве фреймворка для REST я выбрал Jersey. Его легко найти и скачать. Точно так же как и саму IDE. В качестве сервера я установил GlassFish плагин для эклипса. Инструкцию по его установке точно так же легко найти. Ну на этом и всё.

Создание проекта

Итак. У нас есть Eclipse, папка с JAR-файлами Jersey и установленный GlassFish плагин. Теперь запускаем Eclipse и создаем пустой Dynamic Web Project, на последней вкладке не забываем отметить чек-бокс, который отвечает за генерацию web.xml файла.

Eclipse создаст нам пустой проект, который можно уже попробовать запустить на нашем сервере (Run As -> Run On Server).
После запуска появится встроенный браузер и покажет страничку Hello World. Теперь на надо скопировать JAR-файлы Jersey в папку [имя проекта]/ WebContent / WEB-INF / lib. Таким образом мы подключим все нужные библиотеки. Теперь очередь за web.xml файлом. Он находится в [имя проекта]/ WebContent / WEB-INF.
Вот листинг этого файла

<?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_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  <display-name>WebRest</display-name>
  <servlet>
    <servlet-name>Jersey REST Service</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>com.sun.jersey.config.property.packages</param-name>
      <param-value>ru.example.rest.resource</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Jersey REST Service</servlet-name>
    <url-pattern>/rest/*</url-pattern>
  </servlet-mapping>
</web-app> 

Параметр com.sun.jersey.config.property.packages должен указывать у нас тот пакет в котором будут храниться наши файлы ресуры. А в тэге url-pattern хранится путь к нашему веб-сервису. Думаю не стоит рассказывать, что это основополагающий момент RESTful веб сервисов, ведь его сама идеология говорит о том, что каждый ресурс должен иметь свой адрес. Теперь наш веб сервис доступен по адресу localhost:8080/[имя нашего проекта]/rest/. Это будет являться нашим базовым URI.

Создание веб-сервиса

Создание сущностих

В качестве примера я хотел привести простой веб-севрис, который представляет из себя такой мини дневничок. Т.е. у нас есть один тип сущностей Message и с ним мы и будем работать.

Вот класс Message

package ru.example.rest.entity;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Message {
	private long messageId;
	private String messageTitle;
	private String messageText;

	public Message() {

	}

	public Message(long messageId, String messageTitle, String messageText) {

		this.messageId = messageId;
		this.messageTitle = messageTitle;
		this.messageText = messageText;
	}

	public long getMessageId() {
		return messageId;
	}

	public void setMessageId(long messageId) {
		this.messageId = messageId;
	}

	public String getMessageTitle() {
		return messageTitle;
	}

	public void setMessageTitle(String messageTitle) {
		this.messageTitle = messageTitle;
	}

	public String getMessageText() {
		return messageText;
	}

	public void setMessageText(String messageText) {
		this.messageText = messageText;
	}

	@Override
	public String toString() {
		return "Message [messageId=" + messageId + ", messageTitle="
				+ messageTitle + ", messageText=" + messageText + "]";
	}

}

Единственное что тут может вызывать вопрос это аннотация @XmlRootElement. Она нам нужна чтобы Java сама преобразовывала данный класс в XML формат для передачи через HTTP. В остальном это обычный класс с тремя полями. Id сообщения, Title сообщения и Text сообщения.

Создание модели для работы с нашим классом

В качестве контент-провайдера я решил использовать обычный List. На данном этапе мне не хотелось бы углубляться в ORM и хранение данных в таблицах БД, потому что эта тема заслуживает отельной статьи, которая возможно будет следующей. И так я создал служебный класс который будет хранить в себе List наших сообщений и предоставлять методы для работы с этим листом.

Вот листинг класса для работы с данными

package ru.example.rest.model;

import java.util.ArrayList;
import java.util.List;

import ru.example.rest.entity.Message;

public class Data {
	private static List<Message> data;
	private static long count = 5;

	static {
		data = new ArrayList<Message>();
		data.add(new Message(1L, "Hello", "Hello! I'm first entry!"));
		data.add(new Message(2L, "2nd", "second messages"));
		data.add(new Message(3L, "here again!", "some text"));
		data.add(new Message(4L, "HI!", "pam param"));
	}

	public static List<Message> getData() {
		return data;
	}

	public static Message findMessageById(long id) {
		for (Message message : data) {
			if (message.getMessageId() == id) {
				return message;
			}
		}
		return null;
	}

	public static boolean deleteMessageById(long id) {
		boolean result = false;
		for (Message message : data) {
			if (message.getMessageId() == id) {
				result = data.remove(message);
				return result;
			}
		}
		return result;
	}

	public static boolean updateMessage(Message message) {
		boolean result = false;
		for (Message temp: data) {
			if (temp.getMessageId() == message.getMessageId()) {
				temp.setMessageText(message.getMessageText());
				temp.setMessageTitle(message.getMessageTitle());
				result = true;
			}
		}
		return result;
	}

	public static boolean addMesage(Message message) {
		message.setMessageId(count);
		count++;
		return data.add(message);
	}

}

Тут мы просто создаем List и заполняем его несколькими записями. Далее метод getData() возвращает нам ссылку на сам List. Что делают остальные методы легко догадаться по их названиям.

Теперь сам сервис

Сам сервис это ресурс-класс с аннотациями в котором указывается по какому URI какой ресурс или какое действие будет происходить в зависимости от типа запроса. Основные аннотации это
Path(«путь к данному ресурсу») данная аннотация указывает на конкретный адрес класса или метода.
так же есть разновидность @Path("{id}") которая нам говорит о том что id это некая переменная которую мы сможем передать в наш метод.
Consumes (MediaType) и Produces(MediaType) будут говорить нам о получаемых и отправляемых данных соответственно. В нашем случае я выбрал APPLICATION_XML. Не спрашивайте почему не JSON, просто по мне так проще. Не зря же придумали JAX-Bind.
GET — говорит что этот метод будет отправлять данные от сервиса клиенту.
PUT/POST — говорит что этот метод будет добавлять/обновлять данные в наш сервис.
Лично я использую PUT для добавления, а POST для обновления.
DELETE — говорит о том что этот метод будет удалять данные из нашего хранилища

Вот листинг класса ресурса
package ru.example.rest.resource;

import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import ru.example.rest.entity.Message;
import ru.example.rest.model.Data;

@Path("message")
public class MessageResource {

	@GET
	@Produces(MediaType.APPLICATION_XML)
	public List<Message> getAllMessages() {
		List<Message> messages = Data.getData();
		if (messages == null) {
			throw new RuntimeException("Can't load all messages");
		}
		return messages;
	}

	@GET
	@Path("{id}")
	@Produces(MediaType.APPLICATION_XML)
	public Message getMessageById(@PathParam("id") long id) {
		Message message = Data.findMessageById(id);
		if (message == null) {
			throw new RuntimeException("can't find mesage with id = " + id);
		}
		return message;
	}

	@PUT
	@Consumes(MediaType.APPLICATION_XML)
	public void addMessage(Message message) {
		if (Data.addMesage(message) != true) {
			throw new RuntimeException("can't add mesage with id = "
					+ message.getMessageId());
		}
		;
	}

	@DELETE
	@Path("{id}")
	public void deleteMessage(@PathParam("id") long id) {
		if (Data.deleteMessageById(id) != true) {
			throw new RuntimeException("can't delete mesage with id = " + id);
		}
	}

	@POST
	@Consumes(MediaType.APPLICATION_XML)
	public void updateMessage(Message message) {
		if (Data.updateMessage(message) != true) {
			throw new RuntimeException("can't update mesage with id = "
					+ message.getMessageId());
		}
	}

}

Думаю стоит прояснить несколько моментов. Наш класс-ресурс будет доступен по адресу localhost:8080/[имя проекта]/rest/message при запросе типа GET нам будет возвращаться список всех записей. при запросе POST с параметром Message у нас будет обновляться элемент Message. При запросе типа PUT c параметром Message у нас будет добавляться новый message в наше хранилище данных. При обращении к адресу методом типа GET localhost:8080/[имя проекта]/rest/message/id нам будет возвращаться message имеющий номер равный id. А если также обратиться методом DELETE то будет удален message с номером id.

Заключение

Небольшой примерно клиента

Для тестирования данного сервиса я написал небольшой Java клиент. Обычное java приложение которое демонстрирует обращение по всем методам и выводит результаты на консоль.

Вот его листинг

public class RestClient {
	public static void main(String[] args) {
		ClientConfig config = new DefaultClientConfig();
		Client client = Client.create(config);
		WebResource service = client.resource(getBaseURI());

		/*
		 * Get list of messages
		 */
		GenericType<List<Message>> genericType = new GenericType<List<Message>>() {
		};
		List<Message> messages = service.path("rest").path("message")
				.accept(MediaType.APPLICATION_XML).get(genericType);

		for (Message temp : messages) {
			System.out.println(temp);
		}

		/*
		 * Get message by ID
		 */
		long id = 4;
		Message message = service.path("rest").path("message")
				.path(String.valueOf(id)).accept(MediaType.APPLICATION_XML)
				.get(Message.class);
		System.out.println("Message with ID = " + id);
		System.out.println(message);

		/*
		 * Update message
		 */

		message.setMessageTitle("udated title");
		message.setMessageText("updated text");
		service.path("rest").path("message").post(message);
		message = service.path("rest").path("message").path(String.valueOf(id))
				.accept(MediaType.APPLICATION_XML).get(Message.class);
		System.out.println("Message with ID = " + id);
		System.out.println(message);

		/*
		 * Delete message
		 */
		System.out.println("delete message with ID = " + id);
		service.path("rest").path("message").path(String.valueOf(id)).delete();
		messages = service.path("rest").path("message")
				.accept(MediaType.APPLICATION_XML).get(genericType);

		for (Message temp : messages) {
			System.out.println(temp);
		}

		/*
		 * Put message
		 */

		System.out.println("puttin' message");
		message = new Message();
		message.setMessageText("PUT MESSAGE!");
		message.setMessageTitle("Put message");
		service.path("rest").path("message")
				.accept(MediaType.APPLICATION_XHTML_XML).put(message);

		messages = service.path("rest").path("message")
				.accept(MediaType.APPLICATION_XML).get(genericType);

		for (Message temp : messages) {
			System.out.println(temp);
		}

	}

	private static URI getBaseURI() {
		return UriBuilder.fromUri("http://localhost:8080/WebRest").build();
	}

}

Самые интересные строчки тут это

GenericType<List<Message>> genericType = new GenericType<List<Message>>() {};
List<Message> messages = service.path("rest").path("message")
				.accept(MediaType.APPLICATION_XML).get(genericType);

genericType это грубо говоря тип List, он нужен чтобы передавать и получать более сложные структуры используя JAX-B.

В данный клиент необходимо включить так же библиотеки Jersey и включить файл описания класса Message.

И в заключении

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

Код клиента и код сервиса доступны на моем гитхабе ЗДЕСЬ.

Данные которыми я пользовался это статья Vogella и интернет.

Автор: daron666

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js