如何使用grails创建灵活的API

时间:2011-04-28 19:53:51

标签: 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
        }

作为一个例子,我需要在控制器中做的只是渲染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'
          }  
      ]

}

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

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

3 个答案:

答案 0 :(得分: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
    }

过滤器首先验证进入的任何请求(部分存根),然后检查您是否可以通过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属性。如果是这样,它将使用它来编组对象。也可以通过MetaClass覆盖toAPI以允许另一种渲染策略。 (例如,版本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转换器marshallers。

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

答案 1 :(得分:2)

等等,如果您仍然需要对网络用户界面使用该操作,结果仍然 Map

如果我希望API调用返回List,我会在操作中添加@ApiListResult('dunnoInstanceList')注释,并且在API调用中只会从操作结果中获取给定参数。

甚至只需@ApiListResult并选择Map的{​​{1}}密钥。

如果您要重用2.0控制器功能来提供1.0请求,那么版本控制无论如何都会变得复杂。我会添加另外两个注释,例如endsWith('InstanceList'),对于已更改的签名,@Since('2.0')@Till('1.1') - 添加保留旧签名的操作。

答案 2 :(得分:1)

最灵活的api是一种不直接与您的控制器绑定并且具有分离关注的API。请求/响应流程中的Apis是一个跨设计的跨设备问题,因此可以通过工具和实例共享配置,安全性和处理。

因此,api需要作为通信层分离,端点需要解析到通信层(允许系统内的重定向返回到通信层),config / security需要是可以共享的组件可在工具和实例之间重新加载/同步(不是将应用程序版本与api版本分离),等等。

Grails api工具包是这种分离的一个主要例子,它允许更具可扩展性的架构。我会建议玩它并随着它的进展而变得舒适。