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