DelphiMVCFrameWork 源码分析(二)

4 篇文章 0 订阅

路由和控制器(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等内容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值