Валидация данных веб-сервиса на стороне сервера с помощью reflection
Пример реализации валидации входных данных веб-сервиса на стороне сервера с использованием reflection.
Используемые технологии и библиотеки
- Apache CXF 3.1.6
- Spring MVC 4.3.0.Release
1. Описание задачи
Добавить проверку (валидацию) на стороне сервера входящих данных от веб-сервиса используя рефлексию.
2. Структура проекта
Эта статья продолжает код, который описан в статье — Обработка ошибок в SOAP веб-сервисе на стороне сервера. В этой части был добавлен интерфейс ValidationErrorMsg, который описывает новую аннотацию. Валидация будет описана в классе HelloSoap.
3. Validation by reflection
Распространенной задачей при использовании веб-сервисов является валидация данных, которые передаются клиенту. Мы рассмотрим простой случай прихода некорректных значений для объекта Goods из нашей модели данных (будет отсутствовать требуемый параметр name).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package ru.javastudy.ws.soap; import ru.javastudy.ws.annotations.ValidationErrorMsg; import ru.javastudy.ws.exceptions.MyWebserviceException; import ru.javastudy.ws.model.Goods; import javax.jws.WebService; import javax.xml.bind.annotation.XmlElement; import java.lang.reflect.Field; /** * Created for JavaStudy.ru on 10.06.2016. */ @WebService(endpointInterface = "ru.javastudy.ws.soap.WebserviceSEI", serviceName = "HelloSoap") public class HelloSoap implements WebserviceSEI { ..... @Override public Goods createGoods(String id, String name) throws MyWebserviceException { Goods goods = new Goods(); goods.setId(Integer.valueOf(id)); validateValues(goods, Goods.class); return goods; } private void validateValues(Object object, Class<?> clazz) throws MyWebserviceException { if (object == null) { if (clazz.isAnnotationPresent(ValidationErrorMsg.class)) { throw new MyWebserviceException(clazz.getAnnotation(ValidationErrorMsg.class).message()); } } for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(ValidationErrorMsg.class)) { field.setAccessible(true); try { if (field.getAnnotation(XmlElement.class).required() && ( field.get(object) == null || field.get(object).equals("")) ) { throw new MyWebserviceException(field.getAnnotation(ValidationErrorMsg.class).message()); } } catch (IllegalAccessException e) { e.printStackTrace(); } } } } } |
Сервер предоставляет веб-метод createGoods() у которого есть два выходных параметра — id и name. Прежде чем создавать товар, необходимо удостовериться, что введенные данные от клиента верны. В противном случае мы должны передать клиенту информацию о том, какие именно данные он ввел не правильно и почему мы не можем исполнить его запрос. Причем эта информация должна быть информативной, а не просто трейс java ошибки, которую кроме программистов никто не поймет.
В данном примере намеренно не вызывается goods.setName(name) и следовательно у товара будет не инициализирован один параметр, который мы отметим как обязательный.
3.1. Goods — проверяемая модель
В этом классе мы добавим самописную аннотацию @ValidationErrorMsg, а также стандартную @XmlElement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
package ru.javastudy.ws.model; import ru.javastudy.ws.annotations.ValidationErrorMsg; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.io.Serializable; /** * Created for JavaStudy.ru on 11.06.2016. */ @XmlRootElement(name = "goods") @XmlAccessorType(XmlAccessType.FIELD) @ValidationErrorMsg(message = "goods can not be empty") public class Goods implements Serializable { private int id; @XmlElement(required = true) @ValidationErrorMsg(message = "name can't be empty") private String name; public Goods() { } public Goods(int id, String name) { this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Goods{" + "id=" + id + ", name='" + name + '\'' + '}'; } } |
Здесь мы указываем, что элемент name должен быть инициализирован обязательно, а так же помечаем эту поле аннотацией с указанием некого сообщения.
3.2 @ValidationErrorMsg
Одним из приемов при валидации данных является создание собственной аннотации, в которой можно описать необходимые методы. В нашем случае мы опишем один метод, который будет выдавать понятное человеку сообщение вместо exception stack trace.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package ru.javastudy.ws.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Created for JavaStudy.ru on 18.06.2016. */ @Retention(value = RetentionPolicy.RUNTIME) @Target(value = {ElementType.FIELD, ElementType.TYPE}) public @interface ValidationErrorMsg { public String message(); } |
Сначала мы указываем жизненный цикл аннотации — она будет видна на этапе работы программы (в runtime). Далее мы описываем к чему будет применятся аннотация.
3.4. Reflection
Вернемся к валидации товара, в котором не было записано его название (поле name). Для этого в классе HelloSoap есть метод validateValues.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private void validateValues(Object object, Class<?> clazz) throws MyWebserviceException { if (object == null) { if (clazz.isAnnotationPresent(ValidationErrorMsg.class)) { throw new MyWebserviceException(clazz.getAnnotation(ValidationErrorMsg.class).message()); } } for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(ValidationErrorMsg.class)) { field.setAccessible(true); try { if (field.getAnnotation(XmlElement.class).required() && ( field.get(object) == null || field.get(object).equals("")) ) { throw new MyWebserviceException(field.getAnnotation(ValidationErrorMsg.class).message()); } } catch (IllegalAccessException e) { e.printStackTrace(); } } } |
В него передается объект и класс этого объекта. В случае, если объект не существует, то мы проверяем у класса наличие аннотации ValidationErrorMsg. Т.к. для нашего класса Goods эта аннотация существует, то будет выброшено исключение с записанным значением в методе message.
1 2 3 |
@ValidationErrorMsg(message = "goods can not be empty") public class Goods implements Serializable { ... |
Если выше с классом всё в порядке, то переходим к проверке полей данного класса. Мы получаем доступ к каждому полю и проверяем у него аннотацию XmlElement.required(). Если такая аннотация есть и поле пустое, то выбрасываем исключение с значением message для этого поля.
1 2 3 |
@XmlElement(required = true) @ValidationErrorMsg(message = "name can't be empty") private String name; |
4. Запуск и проверка работы валидации
Для упрощения сервер и клиент были объединены в один проект. Для запуска серверной части необходимо запустить веб-сервер (в проекте, доступном в конце статьи используется Tomcat). Для запуска клиентской части нужно выполнить метод JavaStudyWS.main();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package ru.javastudy.ws.main; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; import ru.javastudy.ws.exceptions.MyWebserviceException; import ru.javastudy.ws.model.Goods; import ru.javastudy.ws.soap.WebserviceSEI; /** * Created for JavaStudy.ru on 09.06.2016. */ public class JavaStudyWS { public static void main(String[] args) { testSOAPFromClient(); } /** * create client and test soap service */ private static void testSOAPFromClient() { String soapServiceUrl = "http://localhost:8080/soap/webserviceSEI"; JaxWsProxyFactoryBean factoryBean = new JaxWsProxyFactoryBean(); factoryBean.setServiceClass(WebserviceSEI.class); factoryBean.setAddress(soapServiceUrl); WebserviceSEI webserviceSEI = (WebserviceSEI) factoryBean.create(); Goods result = null; try { result = webserviceSEI.createGoods("123", "SomeName"); } catch (MyWebserviceException e) { e.printStackTrace(); } System.out.println("Result: " + result); } } |
В результате увидим следующую ошибку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
Connected to the target VM, address: '127.0.0.1:64914', transport: 'socket' июл 02, 2016 5:34:04 PM org.apache.cxf.wsdl.service.factory.ReflectionServiceFactoryBean buildServiceFromClass INFO: Creating Service {http://soap.ws.javastudy.ru/}WebserviceSEIService from class ru.javastudy.ws.soap.WebserviceSEI Result: null <strong>ru.javastudy.ws.exceptions.MyWebserviceException: name can't be empty</strong> at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at org.apache.cxf.jaxb.JAXBEncoderDecoder.unmarshallException(JAXBEncoderDecoder.java:522) at org.apache.cxf.jaxb.JAXBEncoderDecoder.unmarshall(JAXBEncoderDecoder.java:663) at org.apache.cxf.jaxb.io.DataReaderImpl.read(DataReaderImpl.java:179) at org.apache.cxf.interceptor.ClientFaultConverter.processFaultDetail(ClientFaultConverter.java:155) at org.apache.cxf.interceptor.ClientFaultConverter.handleMessage(ClientFaultConverter.java:82) at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) at org.apache.cxf.interceptor.AbstractFaultChainInitiatorObserver.onMessage(AbstractFaultChainInitiatorObserver.java:112) at org.apache.cxf.binding.soap.interceptor.CheckFaultInterceptor.handleMessage(CheckFaultInterceptor.java:69) at org.apache.cxf.binding.soap.interceptor.CheckFaultInterceptor.handleMessage(CheckFaultInterceptor.java:34) at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) at org.apache.cxf.endpoint.ClientImpl.onMessage(ClientImpl.java:798) at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponseInternal(HTTPConduit.java:1670) at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponse(HTTPConduit.java:1551) at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1348) at org.apache.cxf.transport.AbstractConduit.close(AbstractConduit.java:56) at org.apache.cxf.transport.http.HTTPConduit.close(HTTPConduit.java:651) at org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor.handleMessage(MessageSenderInterceptor.java:62) at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) at org.apache.cxf.endpoint.ClientImpl.doInvoke(ClientImpl.java:514) at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:423) at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:324) at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:277) at org.apache.cxf.frontend.ClientProxy.invokeSync(ClientProxy.java:96) at org.apache.cxf.jaxws.JaxWsClientProxy.invoke(JaxWsClientProxy.java:139) at com.sun.proxy.$Proxy40.createGoods(Unknown Source) at ru.javastudy.ws.main.JavaStudyWS.testSOAPFromClient(JavaStudyWS.java:31) at ru.javastudy.ws.main.JavaStudyWS.main(JavaStudyWS.java:14) |
Аналогично можно попробовать не создать класс вообще и тогда увидите в сообщении goods can not be empty.