JAX-RS: Java API for RESTful Web Services API'sinin geliştirilmesini temel amaçları olarak aşağıdakiler söylenmektedir :
Rest mimarisindeki HTTP metodları aşağıdaki gibidir :
RESTful ile basit bir Hello Wordl class örneği görülmektedir :
@Path("/helloworld")
public class HelloWorldResource {
@GET
@Produces("text/plain")
public String getClichedMessage() {
return "Hello World";
}
}
Yukarıdaki class bir root resource (kök kaynak)'tır. İçinde veri çekmek amacıyla bir ClickedMessage alt kaynağı yaratılmıştır. ../helloword adresi GET method'u ile çağrıldığında cevap olarak Hello World değeri alınacaktır
RESTFul'de bir web servis, kaynak sınıfı (resource class) ve istekleri işleyen istek metodlarından (request metod) oluşur. En basit kaynak sınıfı (resource class) ve istek metodu (request metod) için aşağıdaki gibi bir örnek verilebilir :
@Path("/helloworld")
public class HelloWorldResource {
@GET
@Produces("text/plain")
public String getClichedMessage() {
return "Hello World";
}
}
Burada HelloWorldResource class'ı bir kaynak sınıfıdır ve getClichedMessage ise bir @GET metodunu destekleyen bir kaynak metodudur. Kaynak sınıfı olması için class ifaesinin üzerine @Path("/helloworld") annotation'su eklenir. @GET ile de (@GET,@POST,@PUT,@DELETE gibi metodlara "Request Method Designator" adı verilmektedir) bir request metodu oluşturulur.
Bir kaynak sınıfı içinde @Path ve @GET,@POST,@PUT,@DELETE gibi (Request Method Designator) tanımlaman bir metod Alt Kaynak Metodu oluşturur. Alt kaynak <üst kaynak pathi>/<alt kaynak path'i> şeklinde erişilir. Aşağıda , yukarıdaki örneği bir alt kaynak metodu eklenmiştir :
@Path("/helloworld")
public class HelloWorldResource {
@GET
@Produces("text/plain")
public String getClichedMessage() {
return "Hello World";
}
@GET
@Produces("text/plain")
@Path("/tr")
public String getClichedMessageTr() {
return "Merhaba";
}
}
Burada getClichedMessageTr adındaki fonksiyon bir alt kaynak metodudur ve bu kaynağa <root>/helloworld/tr ile erişilebilinir. Alt kaynak eğer @GET,@POST,@PUT,@DELETE gibi bir annatation yok ise bunun adına Subresource Locator - Alt Kaynak Bulucusu adı verilir. "Alt Kaynak Bulucusu" başka bir kaynak olan bir nesne döndüren bir alt kaynaktır. Aşağıda Subresource Locator - Alt Kaynak Bulucusu bulunanan bir örnek görülmektedir :
import javax.ws.rs.*;
@Path("/helloworld")
public class HelloWorldResource {
private String languageCode="en";
public HelloWorldResource() {}
public HelloWorldResource(String languageCode) {
this.languageCode=languageCode;
}
@GET
@Produces("text/plain")
public String getClichedMessage() {
if(languageCode.equals("en")){
return "Hello World";
}else if(languageCode.equals("tr")){
return "Merhaba Dunya";
} else{
return "Hello World";
}
}
@GET
@Produces("text/plain")
@Path("/tr")
public String getClichedMessageTr() {
return "Merhaba";
}
@Path("/lng/{language}")
public HelloWorldResource findHelloWorldResource(
@PathParam("language") String languageCode){
return new HelloWorldResource(languageCode);
}
}
Görüldüğü gibi findHelloWorldResource metodu eklenmiştir. findHelloWorldResource bir Alt Kaynak Bulucusudur (Subresource Locator). @Path annotation bölümünde {language} parametresi dinamik yapılmıştır. <root>/helloworld/lng/tr şeklinde çağrıldığında "Merhaba Dunya" , <root>/helloworld/lng/en şeklinde çağrıldığında "Hello World" mesajı dönülecektir. findHelloWorldResource metodu HelloWorldResource kaynağını döndürmektedir. @GET işlevini HelloWorldResource yerine getirmektedir
Aşağıda bir kaynak ve alt kaynak olan bir örnek gösterilmektedir :
@Path("/helloworld")
public class HelloWorldResource {
@GET
@Produces("text/plain")
public String getClichedMessage() {
return "Hello World";
}
@GET
@Produces("text/plain")
@Path("/tr")
public String getClichedMessageTr() {
return "Merhaba Dunya";
}
}
Yukarıdaki örnekte HelloWorldResource bir kaynak (root resource) class'ıdır ve <root>/helloworld şeklinde GET ile erişildiğinde getClichedMessage çalışacaktır. getClichedMessageTr ise bir alt kaynağı gösterir ve bu alt kaynağa GET ile <root>/helloworld/tr şeklinde erişilebilir
Aşağıda Alt Kaynak Bulucusu (Subresource Locator) kullanıldığı bir örnek gösterilmektedir :
package com.test;
import javax.ws.rs.*;
@Path("/helloworld")
public class HelloWorldResource {
private String languageCode="en";
public HelloWorldResource() {}
public HelloWorldResource(String languageCode) {
this.languageCode=languageCode;
}
@GET
@Produces("text/plain")
public String getClichedMessage() {
if(languageCode.equals("en")){
return "Hello World";
}else if(languageCode.equals("tr")){
return "Merhaba Dunya";
} else{
return "Hello World";
}
}
@Path("/lng/{language}")
public HelloWorldResource findHelloWorldResource(
@PathParam("language") String languageCode){
return new HelloWorldResource(languageCode);
}
}
HelloWorldResource bir kaynaktır ve verilen dile göre hello wordl ifadesini metin olarak göndermektedir. HelloWorldResource kaynağına <root>/helloworld GET ile erişildiğinde getClichedMessage metodu çağrılır ve varsayılan olarak ingilizce mesaj dönder. Eğer <root>/helloworld/lng/tr çağrılırsa Türkçe "Merhaba Dunya", <root>/helloworld/lng/en şeklinde çağrılırsa da "Hello World" dönülecektir. findHelloWorldResource metodu bir Alt Kaynak Bulucusu'dur (Subresource Locator) ve geriye HelloWorldResource kaynağını dönmektedir. @Path annotation'da görüldüğü gibi {language} şeklinde dinamik bir değişken görülmektedir ve bu metoda parametre olarak aktarılmaktadır
Aşağıdaki örnek JSON döndüren bir RESTful web servisini çağırmakta ve dönene JSON string'ini ekrana basmaktadır :
Client c = Client.create();
WebResource r = c.resource("http://www.xxx.com/rs/madde_adi/GET");
String response=r.accept(MediaType.APPLICATION_JSON).get(String.class);
System.out.println("response:"+response);
Not : Jersey Client API'sinin 1.14 versiyonu kullanılmıştır
RESTfull servislerinizde GET ve HEAD ile kesinlikle herhangi bir veri ekleme veya güncelleme yapmayınız. Bu metodlar ile sadece veri alınabilmelidir
<servlet> <servlet-name>Jersey REST Service</servlet-name></strong></p> <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>com.test</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet></strong> <servlet-mapping> <servlet-name>Jersey REST Service</servlet-name> <url-pattern>/rest/*</url-pattern> </servlet-mapping>
<dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-servlet</artifactId> <version>1.14</version> </dependency>RESTful isteklerini işlenebilmesi için ServletContainer eklenmesi gerekir. Bunun için web.xml'e aşağıdaki Servlet eklenmelidir :
<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>com.test</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>com.sun.jersey.spi.container.servlet.ServletContainer sınıfı RESTful servislerine gelen istekleri işlemek için gereklidir. com.sun.jersey.config.property.packages özelliği ise web servislerin (daha doğrusu root resource'ların) bulunacağı paket verilmektedir. url-pattern ile /rest/* şeklinde URL'ler rest servisi olarak kabul edilecek ve uygun servis (root resource) aranacaktır
package com.test; import javax.ws.rs.*; @Path("/helloworld") public class HelloWorldResource { @GET @Produces("text/plain") public String getClichedMessage() { return "Hello World"; } }Yukarıda helloworld path'i ile erişilebilecek bir RESTful servisi yaratılmıştır. Servis GET metodu ile çağrıldığında getClichedMessage fonkisyonu çağrılacaktır ve cevap text olarak geri dönecektir. web.xml'de /rest ile path'i verildiği için helloworld servisinin çağrılması için http://
<param-name>com.sun.jersey.config.property.packages</param-name> <param-value>com.test</param-value> </init-param>
Restfull servislerinde karakter sorunu yaşamamak için UTF-8 output üretmesi sağlanabilir. Bunun için @Producer tanımında charset UTF-8 olarak belirtilebilir :
@Produces("application/json;charset=UTF-8")
public String test(){..}
Yukarıda görüldüğü gibi charset değeri UTF-8 yapılmıştır. Bu servisi çağıran client'da Accept header'ının değerini application/json;charset=UTF-8 şeklinde çağırmalıdır
ClientConfig config = new DefaultClientConfig(); config.getClasses().add(MOXyJsonProvider.class); Client client = Client.create(config); WebResource service = client.resource(UriBuilder.fromUri(url).build()); MyEntity[] services = service.accept(MediaType.APPLICATION_JSON) .get(new GenericType<MyEntity[]>() {});Bu örnekte bir servisten MyEntity dizisi alıyoruz. MyEntity sınıfı aşağıdaki gibi olsun:
import java.io.*; import java.util.*; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; public class IadeEntity implements Serializable{ private long id; @XmlJavaTypeAdapter(DateAdapter.class) private Date myDate; public long getId() { return id; } public void setId(long id) { this.id = id; } public Date getMyDate() { return myDate; } public void setMyDate(Date myDate) { this.myDate = myDate; } }DateAdapter sınıfı da aşağıdaki gibidir:
import java.text.SimpleDateFormat; import java.util.Date; import javax.xml.bind.annotation.adapters.XmlAdapter; public class DateAdapter extends XmlAdapter<String, Date> { private SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm"); @Override public String marshal(Date date) throws Exception { return dateFormat.format(date); } @Override public Date unmarshal(String string) throws Exception { return dateFormat.parse(string); } }Bu şekilde web servis cevabındaki dd.MM.yyyy HH:mm formatındaki tarih, MyEntity sınıfındaki mYDate nesnesine bind edilecektir.
<dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-client</artifactId> <version>1.14</version> </dependency>GET ile çağrıldığında bir String döndüren bir REST servisini çağıralım:
import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; public class RestClientTest { public static void main(String[] args) { Client client = Client.create(); WebResource webResource = client.resource("http://localhost:8080/book.rest/rest/helloworld"); ClientResponse response = webResource.accept("text/plain").get(ClientResponse.class); if (response.getStatus() != 200) { System.out.println("fail:" + response.getStatus()); } String output = response.getEntity(String.class); System.out.println("response : "+output); } }http://localhost:8080/book.rest/rest/helloworld GET ile çağrıldığında size Hello World yazısını dönen bir servistir. Cevap text/plain şeklinde dönmektedir. Önce Client nesnesi yaratılıyor. Client'den yeni bir kaynak nesnesi oluşturuluyor. Cevap olarak ClientResponse alacak şekilde get method'u çağrılıyor. Ardından 200 cevabı dönüldüyse (HTTP status kodu 200 başarılı anlamına geliyor) gelen cevap alınıp çıktıya basılıyor.
<dependencies> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>9.4.6.v20170531</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-servlet</artifactId> <version>9.4.6.v20170531</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-util</artifactId> <version>9.4.6.v20170531</version> </dependency> <dependency> <groupId>org.glassfish.jersey.core</groupId> <artifactId>jersey-server</artifactId> <version>2.25.1</version> </dependency> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-servlet-core</artifactId> <version>2.25.1</version> </dependency> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-jetty-http</artifactId> <version>2.25.1</version> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>2.25.1</version> </dependency> </dependencies>Jetty ve Jersey ile ilgili bağımlılıklar eklenmiştir.
import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("msg") public class MyMessage { @GET @Produces(MediaType.TEXT_PLAIN) public String getMessage() { return "My message\n"; } @GET @Path("j") @Produces(MediaType.APPLICATION_JSON) public String getMessageJson() { return "{\"message\":\"My message\"}"; } @GET @Path("jo") @Produces(MediaType.APPLICATION_JSON) public MsgObj getMessageJsonObject() { MsgObj msgObj=new MsgObj(); msgObj.setName("World"); msgObj.setTitle("Hello"); return msgObj; } }Üç tane servis metodu tanımlanmıştır ve GET ile mesaj ve nesne döndürmektedirler. Servis hazır olduğunda göre artık uygulamayı yapabiliriz :
import java.util.logging.Level; import java.util.logging.Logger; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.glassfish.jersey.servlet.ServletContainer; public class Main { public static void main(String[] args) { Server server = new Server(8080); ServletContextHandler ctx = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); ctx.setContextPath("/"); server.setHandler(ctx); ServletHolder serHol = ctx.addServlet(ServletContainer.class, "/rest/*"); serHol.setInitOrder(1); serHol.setInitParameter("jersey.config.server.provider.packages", "com.fibiler.services"); try { server.start(); server.join(); } catch (Exception ex) { Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); } finally { server.destroy(); } } }
import java.io.PrintWriter; import java.io.StringWriter; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mercury.rest.api.ApiResponse; import com.mercury.rest.api.Error; @Provider public class GenericExceptionMapper implements ExceptionMapper<Throwable>{ private static Logger logger=LoggerFactory.getLogger(GenericExceptionMapper.class); @Override public Response toResponse(Throwable e) { logger.error(e.getLocalizedMessage(),e); ApiResponse apiResponse=new ApiResponse(); setHttpStatus(e, apiResponse); apiResponse.setErrorCode("GENERIC_ERROR"); apiResponse.setErrorDesc(e.getMessage()); StringWriter errorStackTrace = new StringWriter(); e.printStackTrace(new PrintWriter(errorStackTrace)); apiResponse.setErrorDetails(errorStackTrace.toString()); apiResponse.setErrorMessage("Bir hata olustu"); return Response.status(apiResponse.getStatus()) .entity(apiResponse) .type(MediaType.APPLICATION_JSON) .build(); } private void setHttpStatus(Throwable ex, ApiResponse errorMessage) { if(ex instanceof WebApplicationException ) { errorMessage.setStatus(((WebApplicationException)ex).getResponse().getStatus()); } else { errorMessage.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); } } }ExceptionMapper
public class ApiResponse { private int status; private String errorCode; private String errorDesc; // develoepr icin kisa aciklama private String errorMessage; //kullanici icin private String errorDetails; // developer icin detail public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public String getErrorCode() { return errorCode; } public void setErrorCode(String errorCode) { this.errorCode = errorCode; } public String getErrorDesc() { return errorDesc; } public void setErrorDesc(String errorDesc) { this.errorDesc = errorDesc; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } public String getErrorDetails() { return errorDetails; } public void setErrorDetails(String errorDetails) { this.errorDetails = errorDetails; } }Bu sınıf cevaplar için kullanılan bir sınıftır. Response.status(apiResponse.getStatus()) .entity(apiResponse) .type(MediaType.APPLICATION_JSON) .build(); ile apiResponse cevap olarak dönülmektedir.
Client client = ClientBuilder.newClient(); HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic("username","testpassword")); client.register(feature); X result = client.target("http://example.com/get").request(MediaType.APPLICATION_JSON).get(X.class);HttpAuthenticationFeature sınıfın basic() yöntemi ile BASIC HTTP Authentication yapılacağı belirtilir ve HttpAuthenticationFeature nesnesi alınır. Bu nesne Client nesnesine register edilir. En son satır ise http://example.com/get URL'ini çağrılır ve sonuç olarak X sınıfına uygun JSON alınır.
List<AbstractResource> list=new ArrayList<>(); AbstractResource resource1 = IntrospectionModeller.createResource(MyService1.class); AbstractResource resource2 = IntrospectionModeller.createResource(MyService2.class); list=Arrays.asList(resource1, resource2); for (AbstractResource abstractResource : list) { String resourcePath=abstractResource.getPath().getValue(); for (AbstractSubResourceMethod srm :abstractResource.getSubResourceMethods()) { String uri = resourcePath + "/" + srm.getPath().getValue(); // Query Param'lar Eklenir List<Parameter> parameters= srm.getParameters(); for (Parameter parameter : parameters) { if(parameter.getSource()==null){ continue; } if(parameter.getSource().name().equals("QUERY")){ if(uri.indexOf("?")==-1){ uri+="?"; }else{ uri+="&"; } uri+=parameter.getSourceName()+"="; } } System.out.println("("+srm.getHttpMethod()+")" + uri + " [return type : " + srm.getReturnType().getName()+"]"); } System.out.println(); }MyService1, MyService2 şeklinde iki tane resource sınıfı var.
IntrospectionModeller.createResource(MyService.class);ile bir Resource sınıfın modeli yüklenir. getSubResourceMethods() yöntemi ile resource sınıfındaki method'ların (sub resource) listesi alınabilir. AbstractSubResourceMethod sınıfından ise yöntemin method'u (GET, POST vb.), yöntemin dönüş tip, parametereleri alınabilir. Biz yukarıdaki örnekte sadece QueryParam tipindeki parametreleri aldık. ENTITY, MATRIX, PATH, COOKIE, HEADER, CONTEXT, FORM gibi farklı parametreler de eklenebilir. Örneği çalıştırdığımızda aşağıdaki gibi bir çıktı elde edebilir :
(GET)/category/search?q= [return type : java.lang.String] (GET)/category/child/{categoryId} [return type : java.lang.String] (PUT)/question//session?member_id= [return type : java.lang.String] (PUT)/question//session/{session_id}/{question_id}/answer/{option_id} [return type : java.lang.String]
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <artifactSet> <excludes> <exclude>junit:junit</exclude> <exclude>jmock:*</exclude> <exclude>*:xml-apis</exclude> <exclude>org.apache.maven:lib:tests</exclude> </excludes> </artifactSet> </configuration> </execution> </executions> </plugin>Bu plugin ile ürettiğiniz jar'ı kullanarak çalıştırdığınızda aşağıdaki hata alınabilir:
org.glassfish.jersey.message.internal.MessageBodyProviderNotFoundException: MessageBodyReader not found for media type=application/json, type=class [com.fibiler.Test;, genericType=class [Lcom.fibiler.TestBunun sebebi maven-shade-plugin, jersey ile ilişkili jar'lardaki META-INF içindeki services klasöründeki tanımları birleştirmemesidir. Bunu çözmek için ServicesResourceTransformer kullanılır. Bu eklediğinde maven-shade-plugin şu hale döner :
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <artifactSet> <excludes> <exclude>junit:junit</exclude> <exclude>jmock:*</exclude> <exclude>*:xml-apis</exclude> <exclude>org.apache.maven:lib:tests</exclude> </excludes> </artifactSet> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" /> </transformers> </configuration> </execution> </executions> </plugin>