本文3.0版本文章
代码已上传Github+Gitee,文末有地址
上回《从壹开始先后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之九 || 依赖注入IoC学习 + AOP界面编程初探》我们说到了依赖注入Autofac的使用,不知道你们对IoC的使用是怎样的感受,我我的表示仍是比较可行的,至少不用本身再关心一个个复杂的实例化服务对象了,直接经过接口就知足需求,固然还有其余的一些功能,我尚未说到,抛砖引玉嘛,你们若是有好的想法,欢迎留言,也能够来群里,你们一块儿学习讨论。昨天在文末我们说到了AOP面向切面编程的定义和思想,我我的简单使用了下,感受主要的思路仍是经过拦截器来操做,就像是一个中间件同样,今天呢,我给你们说两个小栗子,固然,你也能够合并成一个,也能够自定义扩展,由于咱们是真个系列是基于Autofac框架,因此今天主要说的是基于Autofac的Castle动态代理的方法,静态注入的方式之后有时间能够再补充。
时间真快,转眼已经十天过去了,感谢你们的鼓励,批评指正,但愿个人文章,对您有一点点儿的帮助,哪怕是有学习新知识的动力也行,至少至少,能够为之后跳槽增长新的谈资 [哭笑],这些天咱们从面向对象OOP的开发,后又转向了面向接口开发,到分层解耦,如今到了面向切面编程AOP,往下走将会是,分布式,微服务等等,技术真是永无止境啊!好啦,立刻开始动笔。
大神反馈:
一、群里小伙伴 大龄Giser 根据本文,成功的应用在工做中,点赞,欢迎围观:【ABP】面向切面编程(AOP)知识总结html
零、今天完成的深红色部分

1、AOP 之 实现日志记录(服务层)
首先想想,若是有一个需求(这个只是个人一个想法,真实工做中可能用不上),要记录整个项目的接口和调用状况,固然若是只是控制器的话,仍是挺简单的,直接用一个过滤器或者一个中间件,还记得我们开发Swagger拦截权限验证的中间件么,那个就很方便的把用户调用接口的名称记录下来,固然也能够写成一个切面,可是若是想看下与Service或者Repository层的调用状况呢,好像目前我们只能在Service层或者Repository层去写日志记录了,那样的话,不只工程大(固然你能够用工厂模式),并且耦合性瞬间就高了呀,想象一下,若是日志要去掉,关闭,修改,须要改多少地方!您说是否是,好不容易前边的工做把层级的耦合性下降了。别慌,这个时候就用到了AOP和Autofac的Castle结合的完美解决方案了。
通过这么多天的开发,几乎天天都须要引入Nuget包哈,我我的表示也不想再添加了,如今都已经挺大的了(47M固然包括所有dll文件),今天不会辣!其实都是基于昨天的两个Nuget包中已经自动生成的Castle组件。请看如下步骤:
一、定义服务接口与实现类
首先这里使用到了 BlogArticle 的实体类(这里我保留了sqlsugar的特性,没须要的能够手动删除):git
public class BlogArticle
{
/// <summary>
/// 主键
/// </summary>
/// 这里之因此没用RootEntity,是想保持和以前的数据库一致,主键是bID,不是Id
[SugarColumn(IsNullable = false, IsPrimaryKey = true, IsIdentity = true)]
public int bID { get; set; }
/// <summary>
/// 建立人
/// </summary>
[SugarColumn(Length = 60, IsNullable = true)]
public string bsubmitter { get; set; }
/// <summary>
/// 标题blog
/// </summary>
[SugarColumn(Length = 256, IsNullable = true)]
public string btitle { get; set; }
/// <summary>
/// 类别
/// </summary>
[SugarColumn(Length = int.MaxValue, IsNullable = true)]
public string bcategory { get; set; }
/// <summary>
/// 内容
/// </summary>
[SugarColumn(IsNullable = true, ColumnDataType = "text")]
public string bcontent { get; set; }
/// <summary>
/// 访问量
/// </summary>
public int btraffic { get; set; }
/// <summary>
/// 评论数量
/// </summary>
public int bcommentNum { get; set; }
/// <summary>
/// 修改时间
/// </summary>
public DateTime bUpdateTime { get; set; }
/// <summary>
/// 建立时间
/// </summary>
public System.DateTime bCreateTime { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = int.MaxValue, IsNullable = true)]
public string bRemark { get; set; }
/// <summary>
/// 逻辑删除
/// </summary>
[SugarColumn(IsNullable = true)]
public bool? IsDeleted { get; set; }
}
在IBlogArticleServices.cs定义一个获取博客列表接口 ,并在BlogArticleServices实现该接口
public interface IBlogArticleServices :IBaseServices<BlogArticle>
{
Task<List<BlogArticle>> getBlogs();
}
public class BlogArticleServices : BaseServices<BlogArticle>, IBlogArticleServices
{
IBlogArticleRepository dal;
public BlogArticleServices(IBlogArticleRepository dal)
{
this.dal = dal;
base.baseDal = dal;
}
/// <summary>
/// 获取博客列表
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public async Task<List<BlogArticle>> getBlogs()
{
var bloglist = await dal.Query(a => a.bID > 0, a => a.bID);
return bloglist;
}
}
二、在API层中添加对该接口引用
(注意RESTful接口路径命名规范,我这么写只是为了测试)github
/// <summary>
/// 获取博客列表
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("GetBlogs")]
public async Task<List<BlogArticle>> GetBlogs()
{
return await blogArticleServices.getBlogs();
}
三、添加AOP拦截器
在Blog.Core新建文件夹AOP,并添加拦截器BlogLogAOP,并设计其中用到的日志记录Logger方法或者类sql
关键的一些知识点,注释中已经说明了,主要是有如下:
一、继承接口IInterceptor
二、实例化接口IINterceptor的惟一方法Intercept
三、void Proceed();表示执行当前的方法和object ReturnValue { get; set; }执行后调用,object[] Arguments参数对象
四、中间的代码是新建一个类,仍是单写,就很随意了。数据库
/// <summary>
/// 拦截器BlogLogAOP 继承IInterceptor接口
/// </summary>
public class BlogLogAOP : IInterceptor
{
/// <summary>
/// 实例化IInterceptor惟一方法
/// </summary>
/// <param name="invocation">包含被拦截方法的信息</param>
public void Intercept(IInvocation invocation)
{
//记录被拦截方法信息的日志信息
var dataIntercept = $"{DateTime.Now.ToString("yyyyMMddHHmmss")} " +
$"当前执行方法:{ invocation.Method.Name} " +
$"参数是: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n";
//在被拦截的方法执行完毕后 继续执行当前方法
invocation.Proceed();
dataIntercept += ($"被拦截方法执行完毕,返回结果:{invocation.ReturnValue}");
#region 输出到当前项目日志
var path = Directory.GetCurrentDirectory() + @"\Log";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
string fileName = path + $@"\InterceptLog-{DateTime.Now.ToString("yyyyMMddHHmmss")}.log";
StreamWriter sw = File.AppendText(fileName);
sw.WriteLine(dataIntercept);
sw.Close();
#endregion
}
}
提示:这里展现了如何在项目中使用AOP实现对 service 层进行日志记录,若是你想实现异常信息记录的话,很简单,编程
注意,这个方法仅仅是针对同步的策略,若是你的service是异步的,这里获取不到,正确的写法,在文章底部的 GitHub 代码里,由于和 AOP 思想没有直接的关系,这里就不赘述。后端
下边的是完整代码:
/// <summary>
/// 实例化IInterceptor惟一方法
/// </summary>
/// <param name="invocation">包含被拦截方法的信息</param>
public void Intercept(IInvocation invocation)
{
//记录被拦截方法信息的日志信息
var dataIntercept = "" +
$"【当前执行方法】:{ invocation.Method.Name} \r\n" +
$"【携带的参数有】: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n";
try
{
MiniProfiler.Current.Step($"执行Service方法:{invocation.Method.Name}() -> ");
//在被拦截的方法执行完毕后 继续执行当前方法,注意是被拦截的是异步的
invocation.Proceed();
// 异步获取异常,先执行
if (IsAsyncMethod(invocation.Method))
{
//Wait task execution and modify return value
if (invocation.Method.ReturnType == typeof(Task))
{
invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(
(Task)invocation.ReturnValue,
async () => await TestActionAsync(invocation),
ex =>
{
LogEx(ex, ref dataIntercept);
});
}
else //Task<TResult>
{
invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult(
invocation.Method.ReturnType.GenericTypeArguments[0],
invocation.ReturnValue,
async () => await TestActionAsync(invocation),
ex =>
{
LogEx(ex, ref dataIntercept);
});
}
}
else
{// 同步1
}
}
catch (Exception ex)// 同步2
{
LogEx(ex, ref dataIntercept);
}
dataIntercept += ($"【执行完成结果】:{invocation.ReturnValue}");
Parallel.For(0, 1, e =>
{
LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept });
});
_hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait();
}
四、添加到Autofac容器中,实现注入
还记得昨天的容器么,先把拦截器注入,而后对程序集的注入方法中添加拦截器服务便可
builder.RegisterType<BlogLogAOP>();//能够直接替换其余拦截器!必定要把拦截器进行注册
var assemblysServices = Assembly.Load("Blog.Core.Services");
//builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已扫描程序集中的类型注册为提供全部其实现的接口。
builder.RegisterAssemblyTypes(assemblysServices)
.AsImplementedInterfaces()
.InstancePerLifetimeScope()
.EnableInterfaceInterceptors()//引用Autofac.Extras.DynamicProxy;
.InterceptedBy(typeof(BlogLogAOP));//能够直接替换拦截器
注意其中的两个方法
.EnableInterfaceInterceptors()//对目标类型启用接口拦截。拦截器将被肯定,经过在类或接口上截取属性, 或添加 InterceptedBy ()
.InterceptedBy(typeof(BlogLogAOP));//容许将拦截器服务的列表分配给注册。
说人话就是,将拦截器添加到要注入容器的接口或者类之上。
五、运行项目,查看效果
嗯,你就看到这根目录下生成了一个Log文件夹,里边有日志记录,固然记录很简陋,里边是获取到的实体类,你们能够本身根据须要扩展
这里,面向服务层的日志记录就完成了,你们感受是否是很平时的不同?缓存
2、AOP 之 实现接口数据的缓存功能
想想,若是咱们要实现缓存功能,通常我们都是将数据获取到之后,定义缓存,而后在其余地方使用的时候,在根据key去获取当前数据,而后再操做等等,平时都是在API接口层获取数据后进行缓存,今天我们能够试试,在接口以前就缓存下来。
一、定义 Memory 缓存类和接口
老规矩,定义一个缓存类和接口,你会问了,为何上边的日志没有定义,由于我会在以后讲Redis的时候用到这个缓存接口
/// <summary>
/// 简单的缓存接口,只有查询和添加,之后会进行扩展
/// </summary>
public interface ICaching
{
object Get(string cacheKey);
void Set(string cacheKey, object cacheValue);
}
/// <summary>
/// 实例化缓存接口ICaching
/// </summary>
public class MemoryCaching : ICaching
{
//引用Microsoft.Extensions.Caching.Memory;这个和.net 仍是不同,没有了Httpruntime了
private IMemoryCache _cache;
//仍是经过构造函数的方法,获取
public MemoryCaching(IMemoryCache cache)
{
_cache = cache;
}
public object Get(string cacheKey)
{
return _cache.Get(cacheKey);
}
public void Set(string cacheKey, object cacheValue)
{
_cache.Set(cacheKey, cacheValue, TimeSpan.FromSeconds(7200));
}
}
二、定义一个缓存拦截器
仍是继承IInterceptor,并实现Intercept
/// <summary>
/// 面向切面的缓存使用
/// </summary>
public class BlogCacheAOP : IInterceptor
{
//经过注入的方式,把缓存操做接口经过构造函数注入
private ICaching _cache;
public BlogCacheAOP(ICaching cache)
{
_cache = cache;
}
//Intercept方法是拦截的关键所在,也是IInterceptor接口中的惟必定义
public void Intercept(IInvocation invocation)
{
//获取自定义缓存键
var cacheKey = CustomCacheKey(invocation);
//根据key获取相应的缓存值
var cacheValue = _cache.Get(cacheKey);
if (cacheValue != null)
{
//将当前获取到的缓存值,赋值给当前执行方法
invocation.ReturnValue = cacheValue;
return;
}
//去执行当前的方法
invocation.Proceed();
//存入缓存
if (!string.IsNullOrWhiteSpace(cacheKey))
{
_cache.Set(cacheKey, invocation.ReturnValue);
}
}
//自定义缓存键
private string CustomCacheKey(IInvocation invocation)
{
var typeName = invocation.TargetType.Name;
var methodName = invocation.Method.Name;
var methodArguments = invocation.Arguments.Select(GetArgumentValue).Take(3).ToList();//获取参数列表,我最多须要三个便可
string key = $"{typeName}:{methodName}:";
foreach (var param in methodArguments)
{
key += $"{param}:";
}
return key.TrimEnd(':');
}
//object 转 string
private string GetArgumentValue(object arg)
{
if (arg is int || arg is long || arg is string)
return arg.ToString();
if (arg is DateTime)
return ((DateTime)arg).ToString("yyyyMMddHHmmss");
return "";
}
}
注释的很清楚,基本都是状况服务器
三、注入缓存拦截器
ConfigureServices不用动,只须要改下拦截器的名字就行
注意:
//将 TService 中指定的类型的范围服务添加到实现
services.AddScoped<ICaching, MemoryCaching>();//记得把缓存注入!!!数据结构
四、运行,查看效果
你会发现,首次缓存是空的,而后将Repository仓储中取出来的数据存入缓存,第二次使用就是有值了,其余全部的地方使用,都不用再写了,并且也是面向整个程序集合的
五、多个AOP执行顺序问题
在我最新的 Github 项目中,我定义了三个 AOP :除了上边两个 LogAOP和 CacheAOP 之外,还有一个 RedisCacheAOP,而且经过开关的形式在项目中配置是否启用:

那具体的执行顺序是什么呢,这里说下,就是从上至下的顺序,或者能够理解成挖金矿的形式,执行完上层的,而后紧接着来下一个AOP,最后想要回家,就再一个一个跳出去,在往上层走的时候,矿确定就执行完了,就不用再操做了,直接出去,就像 break 同样,能够参考这个动图:

六、无接口如何实现AOP
上边咱们讨论了不少,可是都是接口框架的,
好比:Service.dll 和与之对应的 IService.dll,Repository.dll和与之对应的 IRepository.dll,咱们能够直接在对应的层注入的时候,匹配上 AOP 信息,可是若是咱们没有使用接口怎么办?
这里你们能够安装下边的实验下:
Autofac它只对接口方法 或者 虚virtual方法或者重写方法override才能起拦截做用。
若是没有接口
案例是这样的:
若是咱们的项目是这样的,没有接口,会怎么办:

// 服务层类
public class StudentService
{
StudentRepository _studentRepository;
public StudentService(StudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
public string Hello()
{
return _studentRepository.Hello();
}
}
// 仓储层类
public class StudentRepository
{
public StudentRepository()
{
}
public string Hello()
{
return "hello world!!!";
}
}
// controller 接口调用
StudentService _studentService;
public ValuesController(StudentService studentService)
{
_studentService = studentService;
}

若是是没有接口的单独实体类
public class Love
{
// 必定要是虚方法
public virtual string SayLoveU()
{
return "I ♥ U";
}
}
//---------------------------
//只能注入该类中的虚方法
builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love)))
.EnableClassInterceptors()
.InterceptedBy(typeof(BlogLogAOP));
3、还有其余的一些问题须要考虑
一、能够针对某一层的指定类的指定方法进行操做,这里就不写了,你们能够本身实验
配合Attribute就能够只拦截相应的方法了。由于拦截器里面是根据Attribute进行相应判断的!!
builder.RegisterAssemblyTypes(assembly)
.Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract) .AsImplementedInterfaces()
.InstancePerLifetimeScope()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(QCachingInterceptor));
二、时间问题,阻塞,浪费资源问题等
定义切面有时候是方便,初次使用会很别扭,使用多了,可能会对性能有些许的影响,由于会大量动态生成代理类,性能损耗,是特别高的请求并发,好比万级每秒,仍是不建议生产环节推荐。因此说切面编程要深刻的研究,不可随意使用,我说的也是九牛一毛,你们继续加油吧!
三、静态注入
基于Net的IL语言层级进行注入,性能损耗能够忽略不计,Net使用最多的Aop框架PostSharp(好像收费了;)采用的便是这种方式。
你们能够参考这个博文:https://www.cnblogs.com/mushroom/p/3932698.html
4、结语
今天的讲解就到了这里了,经过这两个小栗子,你们应该能对面向切面编程有一些朦胧的感受了吧,感兴趣的能够深刻的研究,也欢迎一块儿讨论,刚刚在缓存中,我说到了缓存接口,就引入了下次的讲解内容,Redis的高性能缓存框架,内存存储的数据结构服务器,可用做数据库,高速缓存和消息队列代理。下次再见咯~
一、网友好资料
- 带你学习AOP框架之Aspect.Core[1]
5、Github && Gitee