Ответ 1
Хорошо, вот что я сделал до сих пор, что, на мой взгляд, дает мне немного гибкости. Это, вероятно, много, чтобы читать, но любые предложения по улучшениям или изменениям приветствуются!
Пользовательский фильтр
class ApiFilters {
def authenticateService
def filters = {
authenticateApiUsage(uri:"/api/**") {
before = {
if(authenticateService.isLoggedIn() || false){
//todo authenticate apiKey and apiSession
return true
}else{
return false
}
}
after = {
}
afterView = {
}
}
renderProperContent(uri:"/api/**"){
before = {
//may be cpu heavy operation using reflection, initial tests show 100ms was used on first request, 10ms on subsequent.
def controller = grailsApplication.controllerClasses.find { controller ->
controller.logicalPropertyName == controllerName
}
def action = applicationContext.getBean(controller.fullName).class.declaredFields.find{ field -> field.name == actionName }
if(isControllerApiRenderable(controller) || isActionApiRenderable(action)){
if(isActionApiCorrectVersion(action,params.version)){
return true
}else{
render status: 415, text: "unsupported version"
return false
}
}
}
after = { model ->
if (model){
def keys = model.keySet()
if(keys.size() == 1){
model = model.get(keys.toArray()[0])
}
switch(request.format){
case 'json':
render text:model as JSON, contentType: "application/json"
break
case 'xml':
render text:model as XML, contentType: "application/xml"
break
default:
render status: 406
break
}
return false
}
return true
}
}
}
private boolean isControllerApiRenderable(def controller) {
return ApplicationHolder.application.mainContext.getBean(controller.fullName).class.isAnnotationPresent(ApiEnabled)
}
private boolean isActionApiRenderable(def action) {
return action.isAnnotationPresent(ApiEnabled)
}
private boolean isActionApiCorrectVersion(def action, def version) {
Collection<ApiVersion> versionAnnotations = action.annotations.findAll {
it instanceof ApiVersion
}
boolean isCorrectVersion = false
for(versionAnnotation in versionAnnotations){
if(versionAnnotation.value().find { it == version }){
isCorrectVersion = true
break
}
}
return isCorrectVersion
}
Сначала фильтр аутентифицирует любой запрос, входящий в (неполный заглушку), затем проверяет, имеете ли вы доступ к контроллеру и действие через api и что версия api поддерживается для данного действия. Если все эти условия выполнены, то он продолжает преобразовывать модель в json или xml.
Пользовательские аннотации
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEnabled {
}
Это сообщает ApiFilter, если заданному контроллеру или действию grails разрешено выводить данные xml/json. Поэтому, если аннотация @ApiEnabled находится на контроллере или уровне действия, ApiFilter продолжит преобразование json/xml
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
String[] value();
}
Я не совсем уверен, нужна ли мне эта аннотация, но, пожалуйста, добавьте ее здесь ради аргумента. В этой аннотации приводится информация о том, какие версии api поддерживают данное действие. поэтому, если действие поддерживает api версии 0.2 и 0.3, но 0.1 было поэтапно отменено, то все запросы к/api/0.1/не будут выполняться при этом действии. и если мне нужен более высокий уровень контроля над версией api, я всегда могу сделать простой оператор if или switch, например:
if(params.version == '0.2'){
//do something slightly different
} else {
//do the default
}
ApiMarshaller
class ApiMarshaller implements ObjectMarshaller<Converter>{
private final static CONVERT_TO_PROPERTY = 'toAPI'
public boolean supports(Object object) {
return getConverterClosure(object) != null
}
public void marshalObject(Object object, Converter converter) throws ConverterException {
Closure cls = getConverterClosure(object)
try {
Object result = cls(object)
converter.lookupObjectMarshaller(result).marshalObject(result,converter)
}
catch(Throwable e) {
throw e instanceof ConverterException ? (ConverterException)e :
new ConverterException("Error invoking ${CONVERT_TO_PROPERTY} method of object with class " + object.getClass().getName(),e);
}
}
protected Closure getConverterClosure(Object object) {
if(object){
def overrideClosure = object.metaClass?.getMetaMethod(CONVERT_TO_PROPERTY)?.closure
if(!overrideClosure){
return object.metaClass?.hasProperty(object,CONVERT_TO_PROPERTY)?.getProperty(object)
}
return overrideClosure
}
return null
}
}
Этот класс зарегистрирован как объектMarshaller как для конвертеров XML, так и для JSON. Он проверяет, имеет ли объект свойство toAPI. Если это так, он будет использовать это для маршалирования объекта. toAPI также можно переопределить через MetaClass, чтобы разрешить другую стратегию рендеринга. (ex версия 0.1 отображает объект по-другому, чем версия 0.2)
Bootstrap.. связывая все это
log.info "setting json/xml marshalling for api"
def apiMarshaller = new ApiMarshaller()
JSON.registerObjectMarshaller(apiMarshaller)
XML.registerObjectMarshaller(apiMarshaller)
Это все, что нужно сделать, чтобы использовать новую стратегию маршаллинга.
Пример класса домена
class Sample {
String sampleText
static toAPI = {[
id:it.id,
value:it.sampleText,
version:it.version
]}
}
Простой класс домена, который показывает объявление образца API
Контроллер образца
@ApiEnabled
class SampleController {
static allowedMethods = [list: "GET"]
@ApiVersion(['0.2'])
def list = {
def samples = Sample.list()
return [samples:samples]
}
}
Это простое действие при доступе через api затем возвращает формат xml или json, который может или не может быть определен Sample.toAPI(). Если параметр toAPI не определен, тогда он будет использовать маршаллеры-преобразователи grails по умолчанию.
Итак, вот оно. Ребята, что вы думаете? он гибкий в соответствии с моим первоначальным вопросом? Ребята, вы видите какие-либо проблемы с этим дизайном или потенциальными проблемами производительности?