Хорошо, так вот, что я сделал до сих пор, что, я считаю, дает мне немного гибкости.Это, вероятно, много для прочтения, но любые предложения по улучшениям или изменениям приветствуются!
Пользовательский фильтр
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, если данный граальКонтроллер или действие разрешено выводить данные 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
}
}
Этот класс зарегистрирован как objectMarshaller для преобразователей XML и JSON.Он проверяет, есть ли у объекта свойство 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)
Это все, что нужносделано для того, чтобы использовать новую стратегию маршаллинга.
Образец класса домена
class Sample {
String sampleText
static toAPI = {[
id:it.id,
value:it.sampleText,
version:it.version
]}
}
Простой класс домена, который показывает пример объявления toAPI
Sample controller
@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.
Так вот и все.Что, вы парни, думаете?это гибкий в соответствии с моим первоначальным вопросом?Ребята, вы видите какие-либо проблемы с этим дизайном или потенциальные проблемы с производительностью?