Java REST (JAX-RS)
Java ile REST tabanlı web servis oluşturmak için kullanılan JAX-RS kurulumu, kullanımı ve REST API hazırlarken dikkat edilmesi gereken genel kabul görmüş standartlar yer alıyor.
REST Nedir?
REST hakkında detaylı bilgi almak için REST Nedir? yazıma bakmalısın.
JAX-RS Nedir?
Java programlama dili ile REST tabanlı web servis oluşturmak için kullanılan JCP tarafından belirlenen JSR 339, JSR 370, JSR 311 şartnamesidir.
Java REST API oluşturma
Java ile REST tabanlı web servis oluşturmak için JSR 339, 370 veya 311 şartnamesinde belirtilen kuralları yerine getiren Jersey, RESTEasy, Apache CXF kütüphaneleri kullanılabilir.
REST tabanlı web servislerin belirli bir kuralı olmadığından Java Servlet kullanılarak REST API oluşturulabilir.
Kütüphane kullanmak standart-ortak geliştirme yapılması, WADL üretmesi, Java sınıflarını kolayca JSON-XML veri türlerine göre ifade etme olanağı sağlar.
Kütüphaneler aynı şartnameyi yerine getirdiklerinden benzer kullanıma sahiptir.
Java EE tarafından referans olarak kabul edildiğinden Java ile REST API oluşturma işlemini Jersey kütüphanesi ile hazırlayalım.
Kütüphaneyi kullanmak için maven projesi oluşturma, kütüphane dosyalarını pom.xml dosyasına ekleme, web.xml ayarlarını yapma gibi ayarların yapılması gerekir.
Maven projesi oluşturma
Bunun yerine Maven içerisinde yer alan archetype:generate kullanarak önceden hazırlanmış archetype ile hızlıca ayarları yapabiliriz.
mvn archetype:generate
-DarchetypeGroupId=org.glassfish.jersey.archetypes
-DarchetypeArtifactId=jersey-quickstart-webapp
-DgroupId=com.yusufsezer
-DartifactId=JavaRestProjem
-DinteractiveMode=false
Jersey 3 sürümünden sonra paket adlandırması olarak javax yerine jakarta kullanmaktadır.
Jersey 2 sürümü Servlet container (tomcat 9 öncesi) ve Application Server (jakarta desteği olmayan) ile çalışmayacaktır.
Bunun önüne geçmek için maven projesi oluştururken -DarchetypeVersion=2.31 veya 2.4, 2.5, 2.6, 2.7, 2.8, 2.9 parametresini eklemek yeterli olacaktır.
Projeyi derleme
Aşağıdaki komut ile proje derlenir.
mvn package
Derleme işlemi ile target dizinine .war dosyası oluşturulur.
Oluşturulan dosya Servlet Container veya Application Server arayüzü üzerinden yayınlanır.
Servlet Container veya Application Server adresine erişerek proje ile oluşturulan REST adresine (webapi/myresource) erişim sağlanır.
Java sürümünü değiştirme
Oluşturulan maven projesine ait pom.xml dosyası Java 7 sürümüne (1.7) göre oluşturulmaktadır.
Java sürümünü değiştirmek için maven-compiler-plugin ayarını değiştirmek yeterli olacaktır.
<configuration>
<source>21</source>
<target>21</target>
</configuration>
Jersey nasıl çalışır
Maven archetype ile oluturulan proje içerisinde yer alan web.xml incelendiğinde aşağıdaki servlet ayarları görünecektir.
<servlet>
<servlet-name>Jersey Web Application</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>com.yusufsezer</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/webapi/*</url-pattern>
</servlet-mapping>
ServletContainer sınıfı org.glassfish.jersey.servlet yolunda yer alan sıradan bir Java Servlet sınıfıdır.
Sınıf load-on-startup ve 1 değeri ile ilk olarak çalıştırılacağı belirlenmiştir.
Java Servlet sınıfı jersey.config.server.provider.packages ayarı ile JAX-RS sınıflarının yer aldığı paket belirlenmiştir.
Sınıfı sıradan Java Servlet sınıfı gibi çalışarak Java Reflection yardımı ile Java Annotations araması yaparak işlem yapar.
REST API adresini belirlemek için web.xml dosyasında servlet-mapping yapılarak /webapi/ ile başlayan isteklerin Jersey tarafından ele alınması sağlanmıştır.
JAX-RS kullanımı
Proje ile oluşturulan MyResource sınıfı incelendiğinde sınıf başına @Path ile sınıfın çalışacağı adres belirlenmiştir.
Sınıf içerisinde yer alan getIt metoduna eklenen @GET ile metodun çalışacağı HTTP yöntemi @Produces ile çıktı türü belirlenmiştir.
Çıktı olarak XML veya JSON türünden değer döndürmek için MIME türü değeri (application/xml, application/json) veya MediaType sınıfında yer alan değerler (MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON) kullanılabilir.
Proje tekrar derlenip çalıştırıldığında sayfa header bilgisindeki Content-Type değeri belirlenen çıktı değeri olacaktır.
Sınıf kullanımı
JAX-RS düz metin (String) yerine sınıf kullanmak istendiğinde veri türüne göre ek kütüphanelerin kullanılması gerekecektir.
Örnek bir sınıf oluşturalım.
public class Person {
private long id;
private String firstName;
private String lastName;
// constructor-get-set metotlar
}
XML
JAX-RS ile XML çıktısı vermeye çalışalım.
@GET
@Produces(MediaType.APPLICATION_XML)
public Person getIt() {
return new Person(1L, "Yusuf", "Sezer");
}
Projeyi tekrar derleyip REST adresini açtığımızda Internal Server Error hatası Servlet container veya Application Server log ekranında aşağıdaki hata yer alacaktır.
MessageBodyWriter not found for media type=application/xml,
type=class com.yusufsezer.model.Person,
genericType=class com.yusufsezer.model.Person.
Bu hatayı gidermek için sınıfın başına @XmlRootElement eklemek yeterli olacaktır.
XML desteği JDK 9 ile birlikte deprecated olmuş JDK 11 ile programlama dilinden çıkarılmıştır.
JDK 11 sonrası destek için Java SOAP yazımda yer alan XML kütüphanesinin pom.xml dosyasın eklenmesi yeterli olacaktır.
JSON
JAX-RS ile JSON çıktısı vermek için @Produces değeri application/json veya MediaType.APPLICATION_JSON kullanılabilir.
Proje tekrar derlenip çalıştırıldığında aşağıdaki hatayı verecektir.
MessageBodyWriter not found for media type=application/json,
type=class com.yusufsezer.model.Person,
genericType=class com.yusufsezer.model.Person.
Bu hatayı gidermek için pom.xml dosyasında yer alan aşağıdaki kütüphaneye ait yorum komutlarının kaldırılması yeterli olacaktır.
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-binding</artifactId>
</dependency>
@Path kullanımı
@Path ifadesi sınıflar için kullanılabileceği gibi metotlar içinde kullanılabilir.
@GET
@Path("/yusuf")
@Produces(MediaType.APPLICATION_JSON)
public Person getIt() {
return new Person(1L, "Yusuf", "Sezer");
}
Adrese /myresource/yusuf ile erişilecektir.
@Path ifadesi kesin değer alabileceği gibi wildcard ifadesi de alabilir.
Wildcard değerine @PathParam ile ulaşılır.
@GET
@Path("{firstName}")
@Produces(MediaType.APPLICATION_JSON)
public Person getIt(@PathParam("firstName") String firstName) {
System.out.println("Gelen değer: " + firstName);
return new Person(1L, "Yusuf", "Sezer");
}
NOT: @Path(“{firstName}-{lastName}”) gibi birden fazla değer kullanılabilir.
@Path ifadesi ayrıca düzenli ifade (regex) kullanımına imkan verir.
@GET
@Path("{personId:\\d+}")
@Produces(MediaType.APPLICATION_JSON)
public Person getPerson(@PathParam("personId") long personId) {
System.out.println("Gelen değer: " + personId);
return new Person(1L, "Yusuf", "Sezer");
}
@Path içerisine yazılan düzenli ifade ile sadece sayısal değerlerin metodu çalıştırması sağlanmıştır.
Parametre almak
İstemciden gelen parametreleri almak için aşağıdaki annotation ifadeleri kullanılır.
@PathParam
URL parametrelerini almak için kullanılır.
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("{personId}")
public Person getPerson(@PathParam("personId") long id) {
System.out.println("Gelen değer: " + id);
return new Person(1L, "Yusuf", "Sezer");
}
@MatrixParam
Anahtar=değer; ifadelerini almak için kullanılır.
@GET
@Produces(MediaType.APPLICATION_JSON)
public String getProduct(@MatrixParam("secenekler") List<String> secenekler) {
System.out.println("Gelen değer sayısı: " + secenekler.size());
return "-";
}
@QueryParam
URL query parametrelerini(?islem=yazdir) almak için kullanılır.
@GET
@Produces(MediaType.APPLICATION_JSON)
public String getProducts(
@QueryParam("start") int start,
@QueryParam("size") int size
) {
System.out.println("Başlangıç : " + start);
System.out.println("Boyut : " + start);
return "-";
}
@FormParam
HTML form verilerini almak için kullanılır.
@POST
public String createPerson(
@FormParam("firstName") String firstName,
@FormParam("lastName") String lastName
) {
System.out.println("Adı : " + firstName);
System.out.println("Soyadı : " + lastName);
return "-";
}
@HeaderParam
HTTP header bilgilerini almak için kullanılır.
@GET
public String getInfo(@HeaderParam("user-agent") String userAgent) {
return userAgent;
}
@CookieParam
Cookie bilgilerini almak için kullanılır.
@GET
public String getInfo(@CookieParam("JSESSIONID") String cookie) {
return cookie;
}
String yerine Cookie sınıfı kullanılarak cookie ile ilgili diğer bilgilere de erişim sağlanabilir.
@BeanParam
Özel parametre sınıfı için kullanılır.
Parametre sayısı arttıkça metot parametresi genişler.
Bu genişlemeyi sıradan bir Java sınıfı oluşturup bu sınıfı @BeanParam ifadesi ile daha sade hale getirebiliriz.
public class PaginateBean {
@QueryParam("start")
private int start;
@QueryParam("size")
private int size;
// get-set metotları
}
@GET
public String getInfo(@BeanParam PaginateBean paginate) {
int start = paginate.getStart();
int size = paginate.getSize();
return "-";
}
@Context
Aşağıdaki sınıfların REST ile kullanımını sağlar.
- SecurityContext
- Request
- Application
- Configuration
- Providers
- ResourceContext
- ServletConfig
- ServletContext
- HttpServletRequest
- HttpServletResponse
- HttpHeaders
- UriInfo
Örnek kullanım aşağıdaki gibidir.
@GET
public String getInfo(@Context HttpHeaders headers) {
StringBuilder sb = new StringBuilder();
headers
.getCookies()
.values()
.forEach(sb::append);
return sb.toString();
}
İfade sınıf içerisinde de kullanılabilir.
@Context
HttpHeaders headers;
@GET
public String getInfo() {
StringBuilder sb = new StringBuilder();
headers
.getCookies()
.values()
.forEach(sb::append);
return sb.toString();
}
@Context ifadesini kullanarak bir çok değere erişim sağlanabilir.
HTTP yöntemlerini kullanma
JAX-RS HTTP yöntemlerine göre çalışacak metotları belirlemek için aşağıdaki annotation ifadeleri yer almaktadır.
- @GET
- @POST
- @PUT
- @DELETE
- @HEAD
- @OPTIONS
- @PATCH
JAX-RS diğer http yöntemlerine ait annotation oluşturmak için ayrıca HttpMethod annotationu kullanılabilir.
NOT: Ön tanımlı JAX-RS annotation ifadeleri HttpMethod ile oluşturulmuştur.
HTTP durum kodlarını kullanma
REST tabanlı web servislerinde işlem sonucu ile ilgili bilgi almak için HTTP durum kodları kullanılır.
JAX-RS ile HTTP durum kodlarını döndürmek için Response sınıfı kullanılır.
@GET
public Response getPerson() {
return Response
.ok(new Person(1L, "Yusuf", "Sezer"))
.build();
}
veya
@GET
public Response getPerson() {
return Response
.ok()
.entity(new Person(1L, "Yusuf", "Sezer"))
.build();
}
Sınıf içerisinde yer alan created, ok, noContent gibi metotlar kullanılabileceği gibi status metodu da kullanılabilir.
@GET
public Response getPerson() {
return Response
.status(Response.Status.CREATED)
.entity(new Person(1L, "Yusuf", "Sezer"))
.build();
}
@GET
public Response getPerson() {
return Response
.status(Response.Status.NOT_FOUND)
.entity("bulunamadı")
.build();
}
Response sınıfı ayrıca ResponseBuilder sınıfı sayesinde tarayıcıya cookie, language, expires, header gibi bilgileri döndürmek için de kullanılır.
@GET
public Response getPerson() {
return Response
.status(Response.Status.NOT_FOUND)
.entity("bulunamadı")
.build();
}
Subresources – Alt REST API
Bir REST API servisinin alt adresleri de olabilir.
Bu adresler Subresources olarak ifade edilir.
Alt sınıfların kullanımı sıradan sınıfların kullanımı gibidir.
@GET
@Path("/{personId}/info")
public PersonInfo getPersonInfo() {
return new PersonInfo();
}
/myresource/1/info adresine gelen istekler PersonInfo sınıfı tarafından işlenecektir.
İstisna yönetimi
JAX-RS özel hata sınıfı oluşturmak ve oluşan hataları yönetmek için özel istisna yönetimine imkan veren ExceptionMapper arayüzü yer alır.
Özel istisna sınıfının hazırlanması için ilk olarak istisna sınıfı oluşturulur.
public class PersonNotFoundException extends RuntimeException {
public PersonNotFoundException(String message) {
super(message);
}
}
Oluşturulan hata sınıfı ExceptionMapper arayüzünü implement eder.
@Provider
public class PersonNotFoundExceptionMapper implements ExceptionMapper<PersonNotFoundException> {
@Override
public Response toResponse(PersonNotFoundException e) {
return Response
.status(Response.Status.NOT_FOUND)
.type(MediaType.APPLICATION_JSON)
.entity(new ErrorMessage(e.getMessage(), 404))
.build();
}
}
NOT: @Provider kullanımı ve web.xml ile belirtilen paket içerisinde yer aldığına dikkat edilmesi gerekir.
Özel istisna sınıfı sıradan istisna sınıfı gibi kullanılır.
@GET
@Path("{personId}")
public Person getPerson(@PathParam("personId") long id) {
if (id < 1) {
throw new PersonNotFoundException(id + " not found.");
}
return new Person(1L, "Yusuf", "Sezer");
}
Hata sınıfınının çıktı olarak daha anlaşılır olması için ayrıca özel sınıf (ErrorMessage) hazırlanmıştır.
public class ErrorMessage {
String message;
int status;
// constructor-get-set
}
Her istisna için ayrı istisna sınıfı oluşturmak yerine aşağıda yer alan ön tanımlı istisna sınıflarının kullanımı faydalı olacaktır.
- WebApplicationException
- BadRequestException
- NotAuthorizedException
- ForbiddenException
- NotFoundException
- NotAllowedException
- NotAcceptableException
- NotSupportedException
- InternalServerErrorException
- ServiceUnavailableException
Ön tanımlı veya özel istisna sınıflarını ayrı ayrı ele almak yerine Generic bir ExceptionMapper oluşturmak faydalı olacaktır.
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
@Override
public Response toResponse(Throwable e) {
ErrorMessage errorMessage = new ErrorMessage(e.getMessage(), 500);
if (e instanceof NotFoundException) {
errorMessage.setStatus(404);
}
return Response
.status(Response.Status.NOT_FOUND)
.type(MediaType.APPLICATION_JSON)
.entity(errorMessage)
.build();
}
}
HATEOAS
REST tabanlı web servislerin herhangi bir kuralı olmamasının sağladığı avantajlar yanında bazı dezavantajları da vardır.
Oluşturulan web servis ile ilgili herhangi bir bilgi olmadığında REST API kara kutu ve kullanılamaz bir halde olacaktır.
Bunun önüne geçebilmek için dokümantasyon ve REST adreslerine erişimin çıktıya eklenmesi gerekir.
HATEOAS ifadesi bu işlemin adrandırılması için kullanılan bir ifadedir.
Örneğin aşağıdaki gibi bir çıktı elde ettiğimizi varsayalım.
{
"firstName": "Yusuf",
"id": 1,
"lastName": "Sezer"
}
Bu çıktının tekil erişim sayfası, güncelleme ve silme adresinin olmaması belirsizliğe yol açmaktadır.
Çıktının model sınıfına özel link alanı ekleyerek aşağıdaki gibi bir çıktının eklenmesi belirsizliği ortadan kaldıracaktır.
{
"firstName": "Yusuf",
"id": 1,
"lastName": "Sezer",
"links": [
{
"href": "rest-adresi/person/1",
"rel": "_self",
"type": "GET"
},
{
"href": "rest-adresi/person/1",
"rel": "edit",
"type": "PUT"
},
{
"href": "/person/1",
"rel": "delete",
"type": "DELETE"
}
]
}
Content Negotiation
Oluşturulan REST API birden farklı veri formatı döndürebilir.
REST API sınıfındaki bir metot TEXT/PLAIN bir metot XML, diğer metod ise JSON çıktı verebilir.
Tarayıcı ile bu adrese erişim sağlandığında tarayıcını öncelik sırasına göre bir çıktı metot seçimi yapılır.
Bu seçimi POSTMAN, CURL gibi araçlara eklenen Accept parametresi ile değiştirilebilir veya */* ile seçim uygulamaya bırakılabilir.
Converter
JAX-RS gelen özel sınıf parametrelerini kabul etmeyecektir.
Sınıfları ayrıştırabilmek için bir çevrim işlemine ihtiyaç vardır.
JAX-RS içerisinde yer alan Converter özelliği ile özel sınıfların parametre olarak metoda geçirilmesi sağlanır.
Bunun için JAX-RS sınıflara özel converter hazırlamak için ParamConverterProvider arayüzünün implement edilmesi gerekiyor.
Böylece aşağıdaki gibi MyDate sınıfı parametre olarak alınabiliyor.
public String getIt(@PathParam("dateString") MyDate myDate) {
return "My date: " + myDate.toString();
}
Message Body Writer
JAX-RS sınıfları çıktı olarak yazdırmak için MessageBodyWriter arayüzünün implement edilmesi gerekiyor.
İmplement edildikten sonra artık nesne çıktı olarak sunulabiliyor.
@GET
@Produces(MediaType.TEXT_PLAIN)
public Date getDate() {
return Calendar.getInstance().getTime();
}
NOT: JSON ve diğer çıktıları almak için MessageBodyWriter arayüzünü uygulayan sınıflar kullanılır.
Custom Media Types
JAX-RS sadece MediaType veya ön tanımlı MIME türlerine ek özel çıktı vermeyi sağlar.
Özel bir format çıktısı vermek için @Produces(“text/ozel”) gibi bir kullanım yeterli olacaktır.
NOT: Özel çıktı istemci tarafından tanınmadığı durumda beklenmedik durumlar oluşabilir.
Filter
JAX-RS, Java Servlet yapısında yer alan Filter özelliğine benzer bir filter özelliğine sahiptir.
Bu özelliği sayesinde bir istek geldiğinde veya cevap dönüşü yapıldığında ekleme yapılabilir.
Örneğin aşağıdaki filtre özel header eklemek için kullanılabilir.
@Provider
public class PoweredByResponseFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
responseContext.getHeaders().add("X-Powered-By", "Yusuf Sezer");
}
}
Filter özelliği (ContainerRequestFilter, ContainerResponseFilter) ile REST API için yetkilendirme, GZIP sıkıştırma vb. işlemler yapılabilir.
Interceptor
Filtreleme özelliği amacının dışına çıkabilir.
Bu durumlar için benzer işlemi yapan ve sıralama önceliği farklı olan Interceptor özelliği kullanılabilir.
- ReaderInterceptor – ContainerRequestFilter
- WriterInterceptor – ContainerResponseFilter
Interceptor özelliği genellikle GZIP sıkıştırma gibi filtreme içerisinde kullanılan işlemler için kullanılmaktadır.
Sonuç
JAX-RS, Jakarta/Java EE tarafından standartları belirtilen Java ile REST tabanlı API oluşturmak için kullanılan bir şartnamedir.
Bu şartnameye ait özelliklerin öğrenilmesi benzer çalışma yapısına sahip MicroProfile, Jakarta/Java EE, Spring, Spring Boot gibi yapıların öğrenilmesini kolaylaştıracaktır.
JAX-RS ile bir REST API oluştururken HATEOAS ve dokümantasyon (swagger, openapi) gibi yapıları kullanmak istemcilerin REST API’yı kullanımını kolaylaştıracaktır.
JAX-RS örneğine buradan ulaşabilirsiniz.
Java Derslerine buradan ulaşabilirsiniz.
Hayırlı günler dilerim.