如何使用grails创建灵活的API

如何使用grails创建灵活的API,api,grails,Api,Grails,所以有一点背景。我正在创建一个具有相当全面的api的网站。api应该能够处理更改,因此我对api进行了版本控制,api url相当于/api/0.2/$apiKey/$controller/$action/$id 我希望能够重用api和标准html视图的控制器。解决方案最初是在我的所有动作中使用withFormat块(通过在我的动作块中使用的私有函数之间共享) 我不喜欢重复的代码,因此我希望集中使用格式功能。因此,与其让一堆控制器和动作拥有自己的withFormat块,我希望它要么是一个服务(但

所以有一点背景。我正在创建一个具有相当全面的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
        }
例如,我在控制器中要呈现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(这目前是控制器的一个要求),那么这一切都非常好,但是在这个示例中,我只想返回实际的列表!不是包装在hashmap中的列表


有没有人对如何创建一个良好的api功能有任何指导,该功能既健壮又灵活,遵循DRY原则,可以处理版本控制(
/api/0.1/
/api/0.2/
),并且可以根据返回的上下文处理不同的封送方法?任何提示都非常感谢

等等,如果您仍然需要对web UI使用操作,结果仍然有一个
映射

如果我希望API调用返回一个
列表
,我会将
@ApiListResult('dunnoInstanceList')
注释添加到一个操作中,而在API调用中,只会从操作结果中获取给定的参数

甚至只需一个
@ApiListResult
并选择一个
Map
键,该键将
endsWith('InstanceList')


如果您要重用2.0控制器功能来服务1.0请求,那么版本控制将非常复杂。我会添加另外两个注释,比如
@Since('2.0')
,对于更改的签名,
@Till('1.1')
@ActionVersion('list','1.0')def list10={…}
,用于保留遗留签名的操作。

好的,下面是我到目前为止所做的,我相信这给了我相当大的灵活性。这可能是很多阅读,但任何关于改进或更改的建议都非常感谢

自定义过滤器

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
    }
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
    }
}
log.info "setting json/xml marshalling for api"

def apiMarshaller = new ApiMarshaller()

JSON.registerObjectMarshaller(apiMarshaller)
XML.registerObjectMarshaller(apiMarshaller)
@ApiEnabled
class SampleController {

    static allowedMethods = [list: "GET"]

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

}
这将告诉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块或开关语句,例如:

if(params.version == '0.2'){
   //do something slightly different 
} else {
  //do the default
}
apimashaller

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
    }
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
    }
}
log.info "setting json/xml marshalling for api"

def apiMarshaller = new ApiMarshaller()

JSON.registerObjectMarshaller(apiMarshaller)
XML.registerObjectMarshaller(apiMarshaller)
@ApiEnabled
class SampleController {

    static allowedMethods = [list: "GET"]

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

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

示例域类

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

}
class Sample {
  String sampleText

  static toAPI = {[
    id:it.id,
    value:it.sampleText,
    version:it.version
  ]}
}
一个显示toAPI示例声明的简单域类

样本控制器

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
    }
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
    }
}
log.info "setting json/xml marshalling for api"

def apiMarshaller = new ApiMarshaller()

JSON.registerObjectMarshaller(apiMarshaller)
XML.registerObjectMarshaller(apiMarshaller)
@ApiEnabled
class SampleController {

    static allowedMethods = [list: "GET"]

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

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


就这样。你们觉得怎么样?按照我原来的问题,它是否灵活?你们看到这个设计有什么问题或潜在的性能问题吗?

@apirendable是我做的一个自定义注释,我在过滤器中检查它在控制器类或动作/字段本身上的存在。我一直在收集这方面的信息,因为我需要做同样的事情。假设我有一个package.api.0.1.UserController和一个package.api.0.2.UserController,那么我可以将这两个类注册为人工制品,然后定义一个过滤器,根据版本参数将请求转发给适当的用户控制器吗?性能如何?api应该是非常快的,过多的注释会降低性能吗?不是注释本身,但是查询控制器类中的注释会有一个普通的反射开销——不会太多。您可以稍后缓存它或生成代码。无论如何,“让它工作,让它正确,让它快速”-按照这个顺序,没有其他的。)最有价值的性能是您的,而不是CPU的-更清晰的代码将让您工作得更快。您尝试做的很多事情都是使用Grails API工具包完成的。。。不需要注释。是的,我确信自从我在2011年5月提出这个问题以来,已经有了相当多的发展。我并没有试图粗鲁。。。我们试图提供信息,而不是从头开始构建,只需使用Beapi框架。它有这个,还有更多