如何使用grails创建灵活的API

net*_*ain 11 api grails

所以有点背景.我正在创建一个具有相当全面的api的网站.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
        }
Run Code Online (Sandbox Code Playgroud)

举个例子,我在控制器中要做的只是渲染xml或者json是:

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

现在,如果我访问触发此操作列表的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'
          }  
      ]

}
Run Code Online (Sandbox Code Playgroud)

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

有没有人有关于如何创建一个健壮且灵活的良好api功能的任何指针,它遵循DRY原则,可以处理版本控制(/api/0.1/,/api/0.2/),并且可以处理不同的编组方法,具体取决于它的上下文.回?任何提示表示赞赏!

net*_*ain 4

好的,这就是我到目前为止所做的,我相信这给了我相当大的灵活性。这可能有很多内容需要阅读,但我们非常感谢任何有关改进或更改的建议!

定制过滤器

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
    }
Run Code Online (Sandbox Code Playgroud)

过滤器首先验证传入的任何请求(部分存根),然后检查您是否有权通过 api 访问控制器和操作,以及给定操作是否支持该 api 版本。如果满足所有这些条件,则会继续将模型转换为 json 或 xml。

自定义注释

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiEnabled {

}
Run Code Online (Sandbox Code Playgroud)

这告诉 ApiFilter 是否允许给定的 grails 控制器或操作输出 xml/json 数据。因此,如果在控制器或操作级别上找到注释@ApiEnabled,ApiFilter将继续进行json/xml转换

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    String[] value();
}
Run Code Online (Sandbox Code Playgroud)

我不太确定是否需要这个注释,但为了论证,我将其添加到此处。此注释提供有关给定操作支持的 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
}
Run Code Online (Sandbox Code Playgroud)

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
    }
}
Run Code Online (Sandbox Code Playgroud)

此类被注册为 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)
Run Code Online (Sandbox Code Playgroud)

这就是利用新的编组策略所需要做的全部工作。

域类示例

class Sample {
  String sampleText

  static toAPI = {[
    id:it.id,
    value:it.sampleText,
    version:it.version
  ]}
}
Run Code Online (Sandbox Code Playgroud)

一个简单的域类,显示 toAPI 示例声明

样品控制器

@ApiEnabled
class SampleController {

    static allowedMethods = [list: "GET"]

    @ApiVersion(['0.2'])
    def list = {
        def samples = Sample.list()
        return [samples:samples]
    }

}
Run Code Online (Sandbox Code Playgroud)

当通过 api 访问时,这个简单的操作将返回 xml 或 json 格式,该格式可能由 Sample.toAPI() 定义,也可能不定义。如果 toAPI 未定义,那么它将使用默认的 grails 转换器编组器。

就是这样了。你们有什么感想?根据我原来的问题,它是否灵活?你们觉得这个设计有什么问题或者潜在的性能问题吗?