路由和控制器(Controllers and routing)
DelphiMVCFrameWork框架的路由(Router)是通过Delphi的Attribute来实现的,作用于Controler类和方法。这样的实现路由的方法比较高效简单,也可能会缺乏灵活性。但是一般项目的应用也足够了。
DelphiMVCFrameWork框架的应用程序总是包含以下部分(摘自框架文档):
1、一个 Delphi WebBroker Module
2、一个 TMVCEngine(引擎)"Hook" WebModule
3、TMVCEngine容纳一个或多个Controler(继承TMVCController)
4、每个Controler包含一个或多个动作(Action),Action就是Controler的公开方法(public Method),同时伴随着RTTI Attribute(包含Router和其它参数)。
5、(可选)一个或多个中间件(Middleware,实现了接口IMVCMiddleware)的类对象)。
一般控制器和URL对应关系:
URL:www.testserver.com/products/categories/books
Server:www.testserver.com
Controler: products(映射为Controler实例)
Action: categories(映射为Controler方法)
Action Params: books(方法参数,有几种映射方式,见后面Demo)
URL第一个斜杠(slash)后字符串映射为Controler类,第二个斜杠后字符串映射为Controler方法,第三个斜杠后(如果有)映射为方法的参数。
如果方法上有MVCPath Attribute指定了,则按照Attribute指定解析。
路由(Router)
路由是通过RTTI Attribute来实现的。
1、MVCPath
MVCPath是最主要的一个Attribute,这是一个强制性的Attribute。MVCPath作用于Controler和Controler Method。可以通过向导生成Controler,也可以自己写:
type
[MVCPath('/products')] //MVCPath 作用于controller
TProductsController = class(TMVCController)
public
[MVCPath()] //MVCPath 作用于方法(Action),如果为空,也可以省略
procedure GetProducts; //此方法(action) 相当于 /products
[MVCPath('/($productid)')]
procedure GetProductByID; //此方法(action) 相当于 /products/123
end;
2、动作参数(Action Params)处理
有4种处理参数的方法:
- 参数包含在URL中,也就是URL映射参数
- 字符串查询参数
- HTML表单值(Form Values)
- Cookies参数
参数获取格式:Context.Request.Params['ParamName']
参数获取顺序是:
- URL映射参数
- 字符串查询参数
- 表单参数(FORM Params)
- Cookie Field
URL映射参数:
比如:GET /orders/abc/2021/10
[MVCPath('/orders')]
TOrderControler = class(TMVCControler)
public
[MVCPath('/($company')/($year)/($month)']
[MVCHTTPMethod([httpGET])]
procedure GetOrders;
end;
TOrderControler.GetOrders;
var
LCompany: string;
LYear, LMonth: Integer;
begin
...................
LCompany := Context.Request.Params['company']; // '$company': abc
LYear := Context.Request.Params['year'].ToInteger;
LMonth := Context.Request.ParamsAsInteger['month'];
...................
end;
字符串查询参数:
字符串查询参数是通过"?"来表示的,是一种常见的形式。
比如:GET /orders/abc?year=2021?month=10
[MVCPath('/orders/($company)')]
[MVCHTTPMethod([httpGET])]
procedure GetOrders;
............
TOrderControler.GetOrders;
var
LCompany: string;
LYear, LMonth: Integer;
begin
...................
LCompany := Context.Request.Params['company']; // '$company': abc
LYear := Context.Request.Params['year'].ToInteger;
LMonth := Context.Request.ParamsAsInteger['month'];
...................
end;
注意:查询参数形式,也可以通过专门的方法来处理:
Context.Request.QueryStringParam('myparam')
Context.Request.QueryStringParamExists('myparam')
Context.Request.QueryStringParams()。
另外,还有一些注意的地方:
一个动作(方法)可以匹配多个路由。
Arrtibute 的名称和Context.Request.Params[ParamName]的“ParamName”必须匹配。
强类型动作(方法)
DelphiMVCFramework框架可以自动将从MVCPAHT属性获得的值注入。推荐这种方法。
[MVCPath('/orders')]
TOrderControler = class(TMVCControler)
public
[MVCPath('/($company')/($year)/($month)']
[MVCHTTPMethod([httpGET])]
procedure GetOrders(const ACompany: string; const AYear, AMonth: Integer);
end;
TOrderControler.GetOrders(const ACompany: string; const AYear, AMonth: Integer);
var
LCompany: string;
LYear, LMonth: Integer;
begin
...................
LCompany := Context.Request.Params['company']; // '$company': abc
LYear := Context.Request.Params['year'].ToInteger;
LMonth := Context.Request.ParamsAsInteger['month'];
...................
end;
强类型支持以下数据类型:
Integer
Int64
Single
Double
Extended
Boolean
TDate
TTime
TDateTime
String
MVCHTTPMethod attribute
DelphiMVCFramework框架支持的HTTP方法:
Verb(动词) Action(动作) Idempotent(幂等)
GET 用有关资源的信息进行响应 yes
POST 所有提交的数据创建新的数据实例 no
PUT 所有提交的数据创建或更新数据实例 yes
DELETE 删除资源实例 yes
HEAD 获取资源的元数据 yes
MVCProduces attribute
设置HTTP Header 的content-type。如果设置了MVCProduces,如果客户端的请求 http header中没有设置相应的accep,则直接返回 404: Not Found.
MVCConsumes attribute
约定俗成的,一般只有POST和PUT能提交body数据。这个Attribute就是指明了客户端提交的body数据格式。
MVCDoc attribute
这是一个辅助的attribute,用于提供简要描述Controler或Action(方法)。
另外还有一些辅助的功能,
动作过滤器:
procedure OnBeforeAction(AContext: TWebContext;
const AActionName: string;
var AHandled: Boolean); override;
procedure OnAfterAction(AContext: TWebContext;
const AActionName: string); override;
控制器相关:
procedure MVCControllerAfterCreate; override;
procedure MVCControllerBeforeDestroy; override;
以上简要描述了路由、控制器和动作,以及一些想关的功能。
看看路由和控制器源码的实现,重点是URI的参数和控制器的方法参数的对应处理。
在DelphiMVCFrameWork 源码分析(一)_看那山瞧那水的博客-CSDN博客
说过,DelphiMVCFrameWork框架通过 TWebModule的OnBeforeDispatch事件来接受后续处理的
constructor TMVCEngine.Create(const AWebModule: TWebModule; const AConfigAction: TProc<TMVCConfig>;
const ACustomLogger: ILogWriter);
begin
inherited Create(AWebModule);
FWebModule := AWebModule;
FixUpWebModule;
FConfig := TMVCConfig.Create;
.................
procedure TMVCEngine.FixUpWebModule;
begin
FSavedOnBeforeDispatch := FWebModule.BeforeDispatch;
FWebModule.BeforeDispatch := OnBeforeDispatch;
end;
TMVCEngine的Create()方法中,传入TWebModule实例,并设置
FWebModule.BeforeDispatch := OnBeforeDispatch;
OnBeforeDispatch()代码:
procedure TMVCEngine.OnBeforeDispatch(ASender: TObject; ARequest: TWebRequest;
AResponse: TWebResponse; var AHandled: Boolean);
begin
AHandled := False;
{ there is a bug in WebBroker Linux on 10.2.1 tokyo }
// if Assigned(FSavedOnBeforeDispatch) then
// begin
// FSavedOnBeforeDispatch(ASender, ARequest, AResponse, AHandled);
// end;
if IsShuttingDown then
begin
AResponse.StatusCode := http_status.ServiceUnavailable;
AResponse.ContentType := TMVCMediaType.TEXT_PLAIN;
AResponse.Content := 'Server is shutting down';
AHandled := True;
end;
if not AHandled then
begin
try
AHandled := ExecuteAction(ASender, ARequest, AResponse);
if not AHandled then
begin
AResponse.ContentStream := nil;
end;
except
on E: Exception do
begin
Log.ErrorFmt('[%s] %s', [E.Classname, E.Message], LOGGERPRO_TAG);
AResponse.StatusCode := http_status.InternalServerError; // default is Internal Server Error
if E is EMVCException then
begin
AResponse.StatusCode := (E as EMVCException).HTTPErrorCode;
end;
AResponse.Content := E.Message;
AResponse.SendResponse;
AHandled := True;
end;
end;
end;
end;
首先判断WebServer是否下线,然后通过ExecuteAction()来执行动作:
function TMVCEngine.ExecuteAction(const ASender: TObject; const ARequest: TWebRequest;
const AResponse: TWebResponse): Boolean;
var
lParamsTable: TMVCRequestParamsTable;
lContext: TWebContext;
lRouter: TMVCRouter;
lHandled: Boolean;
lResponseContentMediaType: string;
lResponseContentCharset: string;
lRouterMethodToCallName: string;
lRouterControllerClazzQualifiedClassName: string;
lSelectedController: TMVCController;
lActionFormalParams: TArray<TRttiParameter>;
lActualParams: TArray<TValue>;
lBodyParameter: TObject;
begin
Result := False;
if ARequest.ContentLength > FConfigCache_MaxRequestSize then
begin
raise EMVCException.CreateFmt(http_status.RequestEntityTooLarge,
'Request size exceeded the max allowed size [%d KiB] (1)',
[(FConfigCache_MaxRequestSize div 1024)]);
end;
{$IF Defined(BERLINORBETTER)}
ARequest.ReadTotalContent;
// Double check for malicious content-length header
if ARequest.ContentLength > FConfigCache_MaxRequestSize then
begin
raise EMVCException.CreateFmt(http_status.RequestEntityTooLarge,
'Request size exceeded the max allowed size [%d KiB] (2)',
[(FConfigCache_MaxRequestSize div 1024)]);
end;
{$ENDIF}
lParamsTable := TMVCRequestParamsTable.Create;
try
lContext := TWebContext.Create(ARequest, AResponse, FConfig, FSerializers);
try
DefineDefaultResponseHeaders(lContext);
DoWebContextCreateEvent(lContext);
lHandled := False;
lRouter := TMVCRouter.Create(FConfig, gMVCGlobalActionParamsCache);
try // finally
lSelectedController := nil;
try // only for lSelectedController
try // global exception handler
ExecuteBeforeRoutingMiddleware(lContext, lHandled);
if not lHandled then
begin
if lRouter.ExecuteRouting(ARequest.PathInfo,
lContext.Request.GetOverwrittenHTTPMethod { lContext.Request.HTTPMethod } ,
ARequest.ContentType, ARequest.Accept, FControllers,
FConfigCache_DefaultContentType, FConfigCache_DefaultContentCharset,
FConfigCache_PathPrefix, lParamsTable, lResponseContentMediaType,
lResponseContentCharset) then
begin
try
if Assigned(lRouter.ControllerCreateAction) then
lSelectedController := lRouter.ControllerCreateAction()
else
lSelectedController := lRouter.ControllerClazz.Create;
except
on Ex: Exception do
begin
Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
[Ex.Classname, Ex.Message, GetRequestShortDescription(ARequest), 'Cannot create controller'], LOGGERPRO_TAG);
raise EMVCException.Create(http_status.InternalServerError,
'Cannot create controller');
end;
end;
lRouterMethodToCallName := lRouter.MethodToCall.Name;
lRouterControllerClazzQualifiedClassName := lRouter.ControllerClazz.QualifiedClassName;
MVCFramework.Logger.InitThreadVars;
lContext.fActionQualifiedName := lRouterControllerClazzQualifiedClassName + '.'+ lRouterMethodToCallName;
lSelectedController.Engine := Self;
lSelectedController.Context := lContext;
lSelectedController.ApplicationSession := FApplicationSession;
lContext.ParamsTable := lParamsTable;
ExecuteBeforeControllerActionMiddleware(
lContext,
lRouterControllerClazzQualifiedClassName,
lRouterMethodToCallName,
lHandled);
if lHandled then
Exit(True);
lBodyParameter := nil;
lSelectedController.MVCControllerAfterCreate;
try
lHandled := False;
lSelectedController.ContentType := BuildContentType(lResponseContentMediaType,
lResponseContentCharset);
lActionFormalParams := lRouter.MethodToCall.GetParameters;
if (Length(lActionFormalParams) = 0) then
SetLength(lActualParams, 0)
else if (Length(lActionFormalParams) = 1) and
(SameText(lActionFormalParams[0].ParamType.QualifiedName,
'MVCFramework.TWebContext')) then
begin
SetLength(lActualParams, 1);
lActualParams[0] := lContext;
end
else
begin
FillActualParamsForAction(lSelectedController, lContext, lActionFormalParams,
lRouterMethodToCallName, lActualParams, lBodyParameter);
end;
lSelectedController.OnBeforeAction(lContext, lRouterMethodToCallName, lHandled);
if not lHandled then
begin
try
lRouter.MethodToCall.Invoke(lSelectedController, lActualParams);
finally
lSelectedController.OnAfterAction(lContext, lRouterMethodToCallName);
end;
end;
finally
try
lBodyParameter.Free;
except
on E: Exception do
begin
LogE(Format('Cannot free Body object: [CLS: %s][MSG: %s]',
[E.Classname, E.Message]));
end;
end;
lSelectedController.MVCControllerBeforeDestroy;
end;
ExecuteAfterControllerActionMiddleware(lContext,
lRouterControllerClazzQualifiedClassName,
lRouterMethodToCallName,
lHandled);
lContext.Response.ContentType := lSelectedController.ContentType;
fOnRouterLog(lRouter, rlsRouteFound, lContext);
end
else // execute-routing
begin
if Config[TMVCConfigKey.AllowUnhandledAction] = 'false' then
begin
lContext.Response.StatusCode := http_status.NotFound;
lContext.Response.ReasonString := 'Not Found';
fOnRouterLog(lRouter, rlsRouteNotFound, lContext);
raise EMVCException.Create(lContext.Response.ReasonString,
lContext.Request.HTTPMethodAsString + ' ' + lContext.Request.PathInfo, 0,
http_status.NotFound);
end
else
begin
lContext.Response.FlushOnDestroy := False;
end;
end; // end-execute-routing
end; // if not handled by beforerouting
except
on ESess: EMVCSessionExpiredException do
begin
if not CustomExceptionHandling(ESess, lSelectedController, lContext) then
begin
Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
[ESess.Classname, ESess.Message, GetRequestShortDescription(ARequest),
ESess.DetailedMessage], LOGGERPRO_TAG);
lContext.SessionStop;
lSelectedController.ResponseStatus(ESess.HTTPErrorCode);
lSelectedController.Render(ESess);
end;
end;
on E: EMVCException do
begin
if not CustomExceptionHandling(E, lSelectedController, lContext) then
begin
Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
[E.Classname, E.Message, GetRequestShortDescription(ARequest), E.DetailedMessage], LOGGERPRO_TAG);
if Assigned(lSelectedController) then
begin
lSelectedController.ResponseStatus(E.HTTPErrorCode);
lSelectedController.Render(E);
end
else
begin
SendRawHTTPStatus(lContext, E.HTTPErrorCode,
Format('[%s] %s', [E.Classname, E.Message]), E.Classname);
end;
end;
end;
on EIO: EInvalidOp do
begin
if not CustomExceptionHandling(EIO, lSelectedController, lContext) then
begin
Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
[EIO.Classname, EIO.Message, GetRequestShortDescription(ARequest), 'Invalid Op'], LOGGERPRO_TAG);
if Assigned(lSelectedController) then
begin
lSelectedController.ResponseStatus(http_status.InternalServerError);
lSelectedController.Render(EIO);
end
else
begin
SendRawHTTPStatus(lContext, http_status.InternalServerError,
Format('[%s] %s', [EIO.Classname, EIO.Message]), EIO.Classname);
end;
end;
end;
on Ex: Exception do
begin
if not CustomExceptionHandling(Ex, lSelectedController, lContext) then
begin
Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
[Ex.Classname, Ex.Message, GetRequestShortDescription(ARequest), 'Global Action Exception Handler'], LOGGERPRO_TAG);
if Assigned(lSelectedController) then
begin
lSelectedController.ResponseStatus(http_status.InternalServerError);
lSelectedController.Render(Ex);
end
else
begin
SendRawHTTPStatus(lContext, http_status.InternalServerError,
Format('[%s] %s', [Ex.Classname, Ex.Message]), Ex.Classname);
end;
end;
end;
end;
try
ExecuteAfterRoutingMiddleware(lContext, lHandled);
except
on Ex: Exception do
begin
if not CustomExceptionHandling(Ex, lSelectedController, lContext) then
begin
Log.ErrorFmt('[%s] %s [PathInfo "%s"] (Custom message: "%s")',
[Ex.Classname, Ex.Message, GetRequestShortDescription(ARequest), 'After Routing Exception Handler'], LOGGERPRO_TAG);
if Assigned(lSelectedController) then
begin
{ middlewares *must* not raise unhandled exceptions }
lSelectedController.ResponseStatus(http_status.InternalServerError);
lSelectedController.Render(Ex);
end
else
begin
SendRawHTTPStatus(lContext, http_status.InternalServerError,
Format('[%s] %s', [Ex.Classname, Ex.Message]), Ex.Classname);
end;
end;
end;
end;
finally
FreeAndNil(lSelectedController);
end;
finally
lRouter.Free;
end;
finally
DoWebContextDestroyEvent(lContext);
lContext.Free;
end;
finally
lParamsTable.Free;
end;
end;
分析下这个方法的主要部分:
首先判断请求内容的长度是否超长,FConfigCache_MaxRequestSize是配置常量,默认5MB(5*1024*1024, MVCFramework.Commons.pas 单元的 TMVCConstants结构)。
lParamsTable := TMVCRequestParamsTable.Create;
创建请求参数表,类型是TDictionary<string,string>(Key = Param Name; Value = Param Value)。
lContext := TWebContext.Create(ARequest, AResponse, FConfig, FSerializers);
创建WebContext。
DefineDefaultResponseHeaders(lContext);
设置默认响应头:
procedure TMVCEngine.DefineDefaultResponseHeaders(const AContext: TWebContext);
begin
if FConfigCache_ExposeServerSignature and (not IsLibrary) then
AContext.Response.CustomHeaders.Values['Server'] := FConfigCache_ServerSignature;
if FConfigCache_ExposeXPoweredBy then
AContext.Response.CustomHeaders.Values['X-Powered-By'] := 'DMVCFramework ' +
DMVCFRAMEWORK_VERSION;
AContext.Response.RawWebResponse.Date := Now;
end;
DoWebContextCreateEvent(lContext);
调用事件OnWebContextCreate()。
lRouter := TMVCRouter.Create(FConfig, gMVCGlobalActionParamsCache);
开始路由处理。
gMVCGlobalActionParamsCache是全局变量,用于全局缓存动作参数(线程安全),初始化为空:
var
gMVCGlobalActionParamsCache: TMVCStringObjectDictionary<TMVCActionParamCacheItem> = nil;
TMVCStringObjectDictionary和TMVCActionParamCacheItem:
{ This type is thread safe }
TMVCStringObjectDictionary<T: class> = class
private
FMREWS: TMultiReadExclusiveWriteSynchronizer;
protected
fDict: TObjectDictionary<string, T>;
public
constructor Create; virtual;
destructor Destroy; override;
function TryGetValue(const Name: string; out Value: T): Boolean;
procedure Add(const Name: string; Value: T);
end;
TMVCActionParamCacheItem = class
private
FValue: string;
FParams: TList<string>;
public
constructor Create(aValue: string; aParams: TList<string>); virtual;
destructor Destroy; override;
function Value: string;
function Params: TList<string>; // this should be read-only...
end;
TMVCStringObjectDictionary是个线程安全字典列表对象。
TMVCActionParamCacheItem是动作参数缓存对象,作用见后面。
ExecuteBeforeRoutingMiddleware(lContext, lHandled);
调用中间件OnBeforeRouting()事件。
调用 lRouter.ExecuteRouting()方法:
function TMVCRouter.ExecuteRouting(const ARequestPathInfo: string;
const ARequestMethodType: TMVCHTTPMethodType;
const ARequestContentType, ARequestAccept: string;
const AControllers: TObjectList<TMVCControllerDelegate>;
const ADefaultContentType: string;
const ADefaultContentCharset: string;
const APathPrefix: string;
var ARequestParams: TMVCRequestParamsTable;
out AResponseContentMediaType: string;
out AResponseContentCharset: string): Boolean;
var
LRequestPathInfo: string;
LRequestAccept: string;
LRequestContentType: string;
LControllerMappedPath: string;
LControllerMappedPaths: TStringList;
LControllerDelegate: TMVCControllerDelegate;
LAttributes: TArray<TCustomAttribute>;
LAtt: TCustomAttribute;
LRttiType: TRttiType;
LMethods: TArray<TRttiMethod>;
LMethod: TRttiMethod;
LMethodPath: string;
LProduceAttribute: MVCProducesAttribute;
lURLSegment: string;
LItem: String;
// JUST FOR DEBUG
// lMethodCompatible: Boolean;
// lContentTypeCompatible: Boolean;
// lAcceptCompatible: Boolean;
begin
Result := False;
FMethodToCall := nil;
FControllerClazz := nil;
FControllerCreateAction := nil;
LRequestAccept := ARequestAccept;
LRequestContentType := ARequestContentType;
LRequestPathInfo := ARequestPathInfo;
if (Trim(LRequestPathInfo) = EmptyStr) then
LRequestPathInfo := '/'
else
begin
if not LRequestPathInfo.StartsWith('/') then
begin
LRequestPathInfo := '/' + LRequestPathInfo;
end;
end;
//LRequestPathInfo := TNetEncoding.URL.EncodePath(LRequestPathInfo, [Ord('$')]);
LRequestPathInfo := TIdURI.PathEncode(Trim(LRequestPathInfo)); //regression introduced in fix for issue 492
TMonitor.Enter(gLock);
try
//LControllerMappedPaths := TArray<string>.Create();
LControllerMappedPaths := TStringList.Create;
try
for LControllerDelegate in AControllers do
begin
LControllerMappedPaths.Clear;
SetLength(LAttributes, 0);
LRttiType := FRttiContext.GetType(LControllerDelegate.Clazz.ClassInfo);
lURLSegment := LControllerDelegate.URLSegment;
if lURLSegment.IsEmpty then
begin
LAttributes := LRttiType.GetAttributes;
if (LAttributes = nil) then
Continue;
//LControllerMappedPaths := GetControllerMappedPath(LRttiType.Name, LAttributes);
FillControllerMappedPaths(LRttiType.Name, LAttributes, LControllerMappedPaths);
end
else
begin
LControllerMappedPaths.Add(lURLSegment);
end;
for LItem in LControllerMappedPaths do
begin
LControllerMappedPath := LItem;
if (LControllerMappedPath = '/') then
begin
LControllerMappedPath := '';
end;
{$IF defined(TOKYOORBETTER)}
if not LRequestPathInfo.StartsWith(APathPrefix + LControllerMappedPath, True) then
{$ELSE}
if not TMVCStringHelper.StartsWith(APathPrefix + LControllerMappedPath, LRequestPathInfo, True) then
{$ENDIF}
begin
Continue;
end;
// end;
// if (not LControllerMappedPathFound) then
// continue;
LMethods := LRttiType.GetMethods; { do not use GetDeclaredMethods because JSON-RPC rely on this!! }
for LMethod in LMethods do
begin
if LMethod.Visibility <> mvPublic then // 2020-08-08
Continue;
if (LMethod.MethodKind <> mkProcedure) { or LMethod.IsClassMethod } then
Continue;
LAttributes := LMethod.GetAttributes;
if Length(LAttributes) = 0 then
Continue;
for LAtt in LAttributes do
begin
if LAtt is MVCPathAttribute then
begin
// THIS BLOCK IS HERE JUST FOR DEBUG
// if LMethod.Name.Contains('GetProject') then
// begin
// lMethodCompatible := True; //debug here
// end;
// lMethodCompatible := IsHTTPMethodCompatible(ARequestMethodType, LAttributes);
// lContentTypeCompatible := IsHTTPContentTypeCompatible(ARequestMethodType, LRequestContentType, LAttributes);
// lAcceptCompatible := IsHTTPAcceptCompatible(ARequestMethodType, LRequestAccept, LAttributes);
if IsHTTPMethodCompatible(ARequestMethodType, LAttributes) and
IsHTTPContentTypeCompatible(ARequestMethodType, LRequestContentType, LAttributes) and
IsHTTPAcceptCompatible(ARequestMethodType, LRequestAccept, LAttributes) then
begin
LMethodPath := MVCPathAttribute(LAtt).Path;
if IsCompatiblePath(APathPrefix + LControllerMappedPath + LMethodPath,
LRequestPathInfo, ARequestParams) then
begin
FMethodToCall := LMethod;
FControllerClazz := LControllerDelegate.Clazz;
FControllerCreateAction := LControllerDelegate.CreateAction;
LProduceAttribute := GetAttribute<MVCProducesAttribute>(LAttributes);
if LProduceAttribute <> nil then
begin
AResponseContentMediaType := LProduceAttribute.Value;
AResponseContentCharset := LProduceAttribute.Charset;
end
else
begin
AResponseContentMediaType := ADefaultContentType;
AResponseContentCharset := ADefaultContentCharset;
end;
Exit(True);
end;
end;
end; // if MVCPathAttribute
end; // for in Attributes
end; // for in Methods
end;
end; // for in Controllers
finally
LControllerMappedPaths.Free;
end;
finally
TMonitor.Exit(gLock);
end;
end;
参数 AControllers 是在TWebModule.OnCreate事件中添加的Contoler列表。
首先检查URL的路由编码,然后线程同步处理路由信息。
首先处理TControler类本身的Attribute,获取根路由。TControler类本身必须至少有一个MVCPath Attribute,不然引发异常"Controller %s does not have MVCPath attribute"。
如果URL(LRequestPathInfo)不是以MVCPath.Path开头,则忽略,进行下一个检查。
如果URL(LRequestPathInfo)以MVCPath.Path开头,接着检查Controler方法(动作)的路由。
只对“public”和“procedure”的方法进行处理。
IsHTTPMethodCompatible():检查方法类型
IsHTTPContentTypeCompatible():检查内容类型
IsHTTPAcceptCompatible():检查客户端允许类型
IsCompatiblePath(APathPrefix + LControllerMappedPath + LMethodPath,
LRequestPathInfo, ARequestParams);
检查路由是否一致并还回参数列表。
APathPrefix + LControllerMappedPath + LMethodPath,是从MVCPath Attribute获取的路由,
LRequestPathInfo,是从URL获取的路由,
ARequestParams,参数列表。
function TMVCRouter.IsCompatiblePath(
const AMVCPath: string;
const APath: string;
var aParams: TMVCRequestParamsTable): Boolean;
function ToPattern(const V: string; const Names: TList<string>): string;
var
S: string;
begin
Result := V;
for S in Names do
Result := StringReplace(Result, '($' + S + ')', '([' + TMVCConstants.URL_MAPPED_PARAMS_ALLOWED_CHARS + ']*)',
[rfReplaceAll]);
end;
var
lRegEx: TRegEx;
lMatch: TMatch;
lPattern: string;
I: Integer;
lNames: TList<string>;
lCacheItem: TMVCActionParamCacheItem;
begin
if not FActionParamsCache.TryGetValue(AMVCPath, lCacheItem) then
begin
lNames := GetParametersNames(AMVCPath);
lPattern := ToPattern(AMVCPath, lNames);
lCacheItem := TMVCActionParamCacheItem.Create(lPattern, lNames);
FActionParamsCache.Add(AMVCPath, lCacheItem);
end;
if (APath = AMVCPath) or ((APath = '/') and (AMVCPath = '')) then
Exit(True)
else
begin
lRegEx := TRegEx.Create('^' + lCacheItem.Value + '$', [roIgnoreCase, roCompiled, roSingleLine]);
lMatch := lRegEx.Match(APath);
Result := lMatch.Success;
if Result then
begin
for I := 1 to pred(lMatch.Groups.Count) do
begin
aParams.Add(lCacheItem.Params[I - 1], TIdURI.URLDecode(lMatch.Groups[I].Value));
end;
end;
end;
end;
ToPattern(): 设置每个方法(动作)的正则表达式,格式为:路由 = 正则表达式,
这些正则表达式列表缓存在FActionParamsCache。
但是这个正则表达式是个常量,
TMVCConstants.URL_MAPPED_PARAMS_ALLOWED_CHARS =
' àèéùòì''"@\[\]\{\}\(\)\=;&#\.:!\_,%\w\d\x2D\x3A\$';
就是URL中允许的字符,目前这样设置,估计是为了以后的扩展处理吧。
GetParametersNames():
function TMVCRouter.GetParametersNames(const V: string): TList<string>;
var
S: string;
Matches: TMatchCollection;
M: TMatch;
I: Integer;
lList: TList<string>;
begin
lList := TList<string>.Create;
try
S := '\(\$([A-Za-z0-9\_]+)\)';
Matches := TRegEx.Matches(V, S, [roIgnoreCase, roCompiled, roSingleLine]);
for M in Matches do
begin
for I := 0 to M.Groups.Count - 1 do
begin
S := M.Groups[I].Value;
if (Length(S) > 0) and (S.Chars[0] <> '(') then
begin
lList.Add(S);
Break;
end;
end;
end;
Result := lList;
except
lList.Free;
raise;
end;
end;
前面说过,在方法的MVCPath Attribute 中,参数是以"($XXXX)"标识的,所以通过正则表达式可以轻松获取参数名称列表。
.................
begin
lRegEx := TRegEx.Create('^' + lCacheItem.Value + '$', [roIgnoreCase, roCompiled, roSingleLine]);
lMatch := lRegEx.Match(APath);
Result := lMatch.Success;
if Result then
begin
for I := 1 to pred(lMatch.Groups.Count) do
begin
aParams.Add(lCacheItem.Params[I - 1], TIdURI.URLDecode(lMatch.Groups[I].Value));
end;
end;
end;
.............
这部分代码从APath获取参数值列表。
总之,通过方法
IsCompatiblePath(const AMVCPath: string; const APath: string;
var aParams: TMVCRequestParamsTable)
从AMVCPath获取参数名称,从APath获取参数值,然后保存在aParams中。
所以,URL的参数数量和TControler的方法的MVCPath Attribute中的参数数量要匹配,不然会异常。
if IsCompatiblePath(APathPrefix + LControllerMappedPath + LMethodPath,
LRequestPathInfo, ARequestParams) then
begin
FMethodToCall := LMethod; //当前执行的方法(动作)
FControllerClazz := LControllerDelegate.Clazz; //TControler类别
FControllerCreateAction := LControllerDelegate.CreateAction; //默认=nil
//Reponse内容类型
LProduceAttribute := GetAttribute<MVCProducesAttribute>(LAttributes);
if LProduceAttribute <> nil then
begin
AResponseContentMediaType := LProduceAttribute.Value;
AResponseContentCharset := LProduceAttribute.Charset;
end
else
begin
AResponseContentMediaType := ADefaultContentType;
AResponseContentCharset := ADefaultContentCharset;
end;
Exit(True);
end;
回到TMVCEngine.ExecuteAction(), lRouter.ExecuteRouting()成功后,
if Assigned(lRouter.ControllerCreateAction) then
lSelectedController := lRouter.ControllerCreateAction()
else
lSelectedController := lRouter.ControllerClazz.Create;
创建控制器(TControler)实例。
lRouterMethodToCallName := lRouter.MethodToCall.Name;
调用方法的名称;
lRouterControllerClazzQualifiedClassName := lRouter.ControllerClazz.QualifiedClassName;
控制器(TControler)的"合格名称",也就是全名称,包含单元名。(主要是用于注册控制器时用,是可选择项)。
MVCFramework.Logger.InitThreadVars;
默认Log初始化;
lContext.fActionQualifiedName := lRouterControllerClazzQualifiedClassName + '.'+ lRouterMethodToCallName;
lSelectedController.Engine := Self;
lSelectedController.Context := lContext;
lSelectedController.ApplicationSession := FApplicationSession;
lContext.ParamsTable := lParamsTable;
设置相关变量;
ExecuteBeforeControllerActionMiddleware()
执行中间件的OnBeforeControllerAction()事件;
lSelectedController.MVCControllerAfterCreate;
执行控制器OnAfterCreate()事件;
lSelectedController.ContentType := BuildContentType(lResponseContentMediaType,
lResponseContentCharset);
设置Response的Content-type和字符集;
前面说过,参数传递用4种方式:
- 参数包含在URL中,也就是URL映射参数
- 字符串查询参数
- HTML表单值(Form Values)
- Cookies参数
是如何处理这几种传递参数处理的?
lActionFormalParams := lRouter.MethodToCall.GetParameters;
获取控制器方法(动作)参数列表;
if (Length(lActionFormalParams) = 0) then
SetLength(lActualParams, 0)
对应无参数的控制器方法(动作)设计;
else if (Length(lActionFormalParams) = 1) and
(SameText(lActionFormalParams[0].ParamType.QualifiedName,
'MVCFramework.TWebContext')) then
begin
SetLength(lActualParams, 1);
lActualParams[0] := lContext;
end
如果只有一个参数,则必定是TWebContext类型参数;
else
begin
FillActualParamsForAction(lSelectedController, lContext, lActionFormalParams,
lRouterMethodToCallName, lActualParams, lBodyParameter);
end;
对应强类型参数设计的TControler。
这里比较复杂是强类型参数设计的TControler的参数处理。
FillActualParamsForAction():
procedure TMVCEngine.FillActualParamsForAction(const ASelectedController: TMVCController;
const AContext: TWebContext; const AActionFormalParams: TArray<TRttiParameter>;
const AActionName: string; var AActualParams: TArray<TValue>; out ABodyParameter: TObject);
var
lParamName: string;
I: Integer;
lStrValue: string;
lFromBodyAttribute: MVCFromBodyAttribute;
lFromQueryStringAttribute: MVCFromQueryStringAttribute;
lFromHeaderAttribute: MVCFromHeaderAttribute;
lFromCookieAttribute: MVCFromCookieAttribute;
lAttributeInjectedParamCount: Integer;
lInjectedParamValue: string;
lList: IMVCList;
lItemClass: TClass;
begin
ABodyParameter := nil;
lAttributeInjectedParamCount := 0;
SetLength(AActualParams, Length(AActionFormalParams));
for I := 0 to Length(AActionFormalParams) - 1 do
begin
lParamName := AActionFormalParams[I].name;
if Length(AActionFormalParams[I].GetAttributes) > 0 then
begin
// Let's check how to inject this parameter
if TRttiUtils.HasAttribute<MVCFromBodyAttribute>(AActionFormalParams[I], lFromBodyAttribute)
then
begin
Inc(lAttributeInjectedParamCount, 1);
if AActionFormalParams[I].ParamType.QualifiedName <> 'System.string' then
begin
ABodyParameter := TRttiUtils.CreateObject(AActionFormalParams[I].ParamType.QualifiedName);
if TDuckTypedList.CanBeWrappedAsList(ABodyParameter, lList) then
begin
lItemClass := TMVCAbstractSerializer(ASelectedController.Serializer).GetObjectTypeOfGenericList(ABodyParameter.ClassInfo);
ASelectedController.Serializer.DeserializeCollection(ASelectedController.Context.Request.Body,
ABodyParameter, lItemClass, stDefault, [], lFromBodyAttribute.RootNode);
end
else
begin
ASelectedController.Serializer.DeserializeObject(ASelectedController.Context.Request.Body,
ABodyParameter, stDefault, [], lFromBodyAttribute.RootNode);
end;
AActualParams[I] := ABodyParameter;
end
else
begin
AActualParams[I] := ASelectedController.Context.Request.Body;
Continue;
end;
end
else if TRttiUtils.HasAttribute<MVCFromQueryStringAttribute>(AActionFormalParams[I],
lFromQueryStringAttribute) then
begin
Inc(lAttributeInjectedParamCount, 1);
lInjectedParamValue := AContext.Request.QueryStringParam
(lFromQueryStringAttribute.ParamName);
HandleDefaultValueForInjectedParameter(lInjectedParamValue, lFromQueryStringAttribute);
AActualParams[I] := GetActualParam(AActionFormalParams[I], lInjectedParamValue);
end
else if TRttiUtils.HasAttribute<MVCFromHeaderAttribute>(AActionFormalParams[I],
lFromHeaderAttribute) then
begin
Inc(lAttributeInjectedParamCount, 1);
lInjectedParamValue := AContext.Request.GetHeader(lFromHeaderAttribute.ParamName);
HandleDefaultValueForInjectedParameter(lInjectedParamValue, lFromHeaderAttribute);
AActualParams[I] := GetActualParam(AActionFormalParams[I], lInjectedParamValue);
end
else if TRttiUtils.HasAttribute<MVCFromCookieAttribute>(AActionFormalParams[I],
lFromCookieAttribute) then
begin
Inc(lAttributeInjectedParamCount, 1);
lInjectedParamValue := AContext.Request.Cookie(lFromCookieAttribute.ParamName);
HandleDefaultValueForInjectedParameter(lInjectedParamValue, lFromCookieAttribute);
AActualParams[I] := GetActualParam(AActionFormalParams[I], lInjectedParamValue);
end
else
begin
raise EMVCException.Create(http_status.InternalServerError,
'Unknown custom attribute on action parameter: ' + AActionFormalParams[I].name +
'. [HINT: Allowed attributes are MVCFromBody, MVCFromQueryString, MVCFromHeader, MVCFromCookie]');
end;
Continue;
end;
// From now on we'll check for url mapped parameters
if not AContext.Request.SegmentParam(lParamName, lStrValue) then
raise EMVCException.CreateFmt(http_status.BadRequest,
'Invalid parameter %s for action %s (Hint: Here parameters names are case-sensitive)',
[lParamName, AActionName]);
AActualParams[I] := GetActualParam(AActionFormalParams[I], lStrValue);
end;
if (AContext.Request.SegmentParamsCount + lAttributeInjectedParamCount) <>
Length(AActionFormalParams) then
raise EMVCException.CreateFmt(http_status.BadRequest,
'Parameters count mismatch (expected %d actual %d) for action "%s"',
[Length(AActionFormalParams), AContext.Request.SegmentParamsCount, AActionName]);
end;
参数可以标志为3种:
FormBody、FromQueryString、FromHeader。
题外话:Delphi的Attribute可以作用于方法中的参数,比如:
type
SameAttr = class(TCustomAttribute)
end;
TMyObj = class
public
procedure TestOne(const [SameAttr] AValue: string);
end;
框架定义了3种Attribute:
MVCFromBodyAttribute:从Request中获取参数
MVCFromQueryStringAttribute:从查询字符串方式中获取参数(比如:GET /orders/abc?year=2021?month=10)
MVCFromHeaderAttribute:从Reques Header中获取参数。
这个方法就不展开分析了,比较啰嗦,但是代码不难看懂。
回到主流程TMVCEngine.ExecuteAction(),
lSelectedController.OnBeforeAction(lContext, lRouterMethodToCallName, lHandled);
调用控制器的OnBeforeAction();
lRouter.MethodToCall.Invoke(lSelectedController, lActualParams);
这里是真正调用方法了。
lSelectedController.OnAfterAction(lContext, lRouterMethodToCallName);
调用控制器的OnAfterAction();
然后lSelectedController.MVCControllerBeforeDestroy;
ExecuteAfterControllerActionMiddleware();
调用中间件的OnAfterControllerAction()事件;
基本就是这样的主流程,其它异常的情况没有分析。
后面还有验证与授权,中间件,JWT以及ActivedRecord等内容。