【问题标题】:How to create a flexible API with grails如何使用 grails 创建灵活的 API
【发布时间】:2011-04-28 19:53:51
【问题描述】:

所以有点背景。我正在创建一个具有相当全面的 api 的网站。 api应该能够处理更改,因此我对api进行了版本化,api url相当于/api/0.2/$apiKey/$controller/$action/$id

我希望能够为 api 以及标准 html 视图重用我的控制器。解决方案首先是在我的所有操作中使用 withFormat 块(通过在我的操作块中使用的私有函数之间共享)。

我不喜欢重复的代码,因此我想集中 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- type: application/json) 那么响应将被编码如下:

{

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

}

如果我总是想返回一个哈希图(目前这是控制器的要求),这一切都很好,但在这个例子中,我只想返回实际的列表!不是包含在 hashmap 中的列表....

是否有人对如何创建一个强大且灵活且遵循 DRY 原则、可以处理版本控制(/api/0.1//api/0.2/)以及可以处理不同编组的良好 api 功能有任何指示方法取决于它返回的上下文?任何提示表示赞赏!

【问题讨论】:

  • @ApiRenderable 是我制作的自定义注释,在过滤器中我检查它是否存在于控制器类或操作/字段本身上。
  • 我一直在收集这方面的信息,因为我需要在路上做同样的事情。 trac.maflt.org/web/ibidem/ticket/180
  • 假设我有一个 package.api.0.1.UserController 和一个 package.api.0.2.UserController 我可以将这两个类注册为人工制品,然后定义一个过滤器,将请求转发到正确的基于版本参数的用户控制器?

标签: api grails


【解决方案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
    }
}

此类已注册为 XML 和 JSON 转换器的 objectMarshaller。它检查对象是否具有 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 示例声明的简单域类

样品控制器

@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 转换器编组器。

所以,就是这样。你们有什么感想?根据我原来的问题是否灵活?你们认为这种设计有什么问题或潜在的性能问题吗?

【讨论】:

  • 您尝试做的很多事情都是使用 Grails API 工具包完成的......无需注释。
  • 是的,我相信自从我在 2011 年 5 月提出这个问题以来,已经有了很多进展。
  • 不想表现得粗鲁……只是想提供信息。
  • 与其从头开始构建这些,不如使用 Beapi Api 框架。它有这个以及更多github.com/orubel/Beapi-API-Framework
【解决方案2】:

等一下,如果您仍然需要对 Web UI 使用该操作,结果仍然必须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 = {...} - 用于保留旧签名的操作。

【讨论】:

  • 注释性能如何?一个api应该是很快的,过多的注解会降低性能吗?
  • 不是注解本身,而是查询控制器类的注解会有一个普通的反射开销——不算太多。您可以稍后缓存它或生成代码。无论如何,“让它工作,让它正确,让它快速” - 按照这个顺序,没有其他 %) 最有价值的性能是你的,而不是 CPU 的 - 更清晰的代码会让你工作得更快。
猜你喜欢
  • 2021-04-28
  • 2011-05-02
  • 2018-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多