Как создать гибкий API с граалями - PullRequest
11 голосов
/ 28 апреля 2011

Итак, немного фона. Я создаю веб-сайт с довольно полным API. API должен быть в состоянии обрабатывать изменения, поэтому я установил версию API с URL-адресом, эквивалентным чему-то вроде /api/0.2/$apiKey/$controller/$action/$id.

Я хочу иметь возможность повторно использовать мои контроллеры для API, а также стандартное представление HTML. Первым решением было использование блока withFormat во всех моих действиях (с помощью закрытой функции, используемой в моих блоках действий).

Мне не нравится повторяющийся код, и поэтому я хочу централизовать его с помощью функциональных возможностей. поэтому вместо того, чтобы иметь кучу контроллеров и действий, имеющих свой собственный блок withFormat, я бы хотел, чтобы это был либо сервис (однако у нас нет доступа к render() в сервисах, не так ли?), либо фильтр, который может отобразить вывод в соответствии с согласованием содержимого Grails.

В моем текущем решении этот фильтр определен:

            after = { model ->
            def controller = grailsApplication.controllerClasses.find { controller ->
                controller.logicalPropertyName == controllerName
            }
            def action = applicationContext.getBean(controller.fullName).class.declaredFields.find{ field -> field.name == actionName }

            if(model && (isControllerApiRenderable(controller) || isActionApiRenderable(action))){
                switch(request.format){
                    case 'json':
                        render text:model as JSON, contentType: "application/json"
                        return false
                    case 'xml':
                        render text:model as XML, contentType: "application/xml"
                        return false
                    default:
                        render status: 406
                        return false
                }
            }
            return true
        }

В качестве примера, все, что мне нужно сделать в контроллере для рендеринга XML или JSON:

@ApiRenderable
def list = {
  def collectionOfSomething = SomeDomain.findAllBySomething('someCriteria')
  return [someCollection:collectionOfSomething]
}

теперь, если я получу доступ к URL, который запускает этот список действий, (/api/0.2/apikey/controller/list.json или /api/0.2/apikey/controller/list?format=json или с заголовками: content- введите: application / json), тогда ответ будет закодирован следующим образом:

{

      someCollection: [
          {
              someData: 'someData'
          },
          {
              someData: 'someData2'
          }  
      ]

}

Это все очень хорошо, если я всегда хочу вернуть хэш-карту (которая в настоящее время является требованием контроллеров), но в этом примере все, что я хотел вернуть, это фактический список! не список в хэш-карте ....

Есть ли у кого-нибудь указания о том, как создать хорошую API-функциональность, которая является надежной и гибкой, и следует принципу DRY, который может обрабатывать версии (/api/0.1/, /api/0.2/) и который может обрабатывать различные сортировки подходит в зависимости от контекста, в котором он возвращается? Любые советы приветствуются!

Ответы [ 3 ]

4 голосов
/ 01 мая 2011

Хорошо, так вот, что я сделал до сих пор, что, я считаю, дает мне немного гибкости.Это, вероятно, много для прочтения, но любые предложения по улучшениям или изменениям приветствуются!

Пользовательский фильтр

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, если данный граальКонтроллер или действие разрешено выводить данные 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
    }
}

Этот класс зарегистрирован как objectMarshaller для преобразователей XML и JSON.Он проверяет, есть ли у объекта свойство toAPI.Если это так, он будет использовать это, чтобы маршалировать объект.toAPI также может быть переопределен через MetaClass, чтобы разрешить другую стратегию рендеринга.(бывшая версия 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
  ]}
}

Простой класс домена, который показывает пример объявления toAPI

Sample controller

@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.

Так вот и все.Что, вы парни, думаете?это гибкий в соответствии с моим первоначальным вопросом?Ребята, вы видите какие-либо проблемы с этим дизайном или потенциальные проблемы с производительностью?

2 голосов
/ 29 апреля 2011

Подождите, если вам все еще нужно использовать действие для веб-интерфейса, результат все равно имеет , чтобы быть Map.

Если бы я хотел, чтобы вызов API возвратил List, я бы добавил аннотацию @ApiListResult('dunnoInstanceList') к действию, а в вызове API просто взял бы данный параметр из результата действия.

Или даже просто @ApiListResult и выберите Map ключ, который endsWith('InstanceList').

Управление версиями будет в любом случае сложным, если вы собираетесь повторно использовать функциональность контроллеров 2.0 для обслуживания 1.0 запросов.Я бы добавил еще пару аннотаций, таких как @Since('2.0') и, для измененных подписей, @Till('1.1') и @ActionVersion('list', '1.0') def list10 = {...} - для действия, которое сохраняет унаследованную подпись.

1 голос
/ 24 августа 2014

Самый гибкий API - это тот, который не связан напрямую с вашими контроллерами и имеет разделение задач.Apis в потоке запросов / ответов является архитектурной сквозной задачей и, таким образом, совместно использует конфигурацию, безопасность и обработку с помощью инструментов и экземпляров.

Таким образом, API должен быть отделен как коммуникационный уровень, конечная точка должнапреобразование в коммуникационный уровень (позволяя перенаправления внутри системы BACK на коммуникационный уровень), конфигурация / безопасность должны быть общим компонентом, который можно повторно загружать / синхронизировать между инструментами и экземплярами (не для разделения версий приложений и версий API) и т. д..

Инструментарий API Grails является ярким примером такого разделения, позволяющего создать гораздо более масштабируемую архитектуру.Я бы посоветовал поиграть с ним и чувствовать себя комфортно по мере его продвижения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...