所以有点背景。我正在创建一个具有相当全面的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/
),以及可以处理不同的编组方法,具体取决于返回的上下文?任何提示都很感激!
答案 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工具包是这种分离的一个主要例子,它允许更具可扩展性的架构。我会建议玩它并随着它的进展而变得舒适。