首页 技术 正文
技术 2022年11月21日
0 收藏 632 点赞 3,515 浏览 10959 个字

本文梯子

正文

本文3.0版本文章

https://mp.weixin.qq.com/s/pjvleNGi_AazZ7COdxQyPQ 

代码已上传Github+Gitee,文末有地址

  上回《从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之九 || 依赖注入IoC学习 + AOP界面编程初探》咱们说到了依赖注入Autofac的使用,不知道大家对IoC的使用是怎样的感觉,我个人表示还是比较可行的,至少不用自己再关心一个个复杂的实例化服务对象了,直接通过接口就满足需求,当然还有其他的一些功能,我还没有说到,抛砖引玉嘛,大家如果有好的想法,欢迎留言,也可以来群里,大家一起学习讨论。昨天在文末咱们说到了AOP面向切面编程的定义和思想,我个人简单使用了下,感觉主要的思路还是通过拦截器来操作,就像是一个中间件一样,今天呢,我给大家说两个小栗子,当然,你也可以合并成一个,也可以自定义扩展,因为我们是真个系列是基于Autofac框架,所以今天主要说的是基于Autofac的Castle动态代理的方法,静态注入的方式以后有时间可以再补充。  时间真快,转眼已经十天过去了,感谢大家的鼓励,批评指正,希望我的文章,对您有一点点儿的帮助,哪怕是有学习新知识的动力也行,至少至少,可以为以后跳槽增加新的谈资 [哭笑],这些天我们从面向对象OOP的开发,后又转向了面向接口开发,到分层解耦,现在到了面向切面编程AOP,往下走将会是,分布式,微服务等等,技术真是永无止境啊!好啦,马上开始动笔。

大神反馈:

1、群里小伙伴 大龄Giser 根据本文,成功的应用在工作中,点赞,欢迎围观:【ABP】面向切面编程(AOP)知识总结

 

零、今天完成的深红色部分

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

一、AOP 之 实现日志记录(服务层)

首先想一想,如果有一个需求(这个只是我的一个想法,真实工作中可能用不上),要记录整个项目的接口和调用情况,当然如果只是控制器的话,还是挺简单的,直接用一个过滤器或者一个中间件,还记得咱们开发Swagger拦截权限验证的中间件么,那个就很方便的把用户调用接口的名称记录下来,当然也可以写成一个切面,但是如果想看下与Service或者Repository层的调用情况呢,好像目前咱们只能在Service层或者Repository层去写日志记录了,那样的话,不仅工程大(当然你可以用工厂模式),而且耦合性瞬间就高了呀,想象一下,如果日志要去掉,关闭,修改,需要改多少地方!您说是不是,好不容易前边的工作把层级的耦合性降低了。别慌,这个时候就用到了AOP和Autofac的Castle结合的完美解决方案了。  经过这么多天的开发,几乎每天都需要引入Nuget包哈,我个人表示也不想再添加了,现在都已经挺大的了(47M当然包括全部dll文件),今天不会辣!其实都是基于昨天的两个Nuget包中已经自动生成的Castle组件。请看以下步骤:

1、定义服务接口与实现类

首先这里使用到了 BlogArticle 的实体类(这里我保留了sqlsugar的特性,没需要的可以手动删除):

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

    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; }    }

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存在IBlogArticleServices.cs定义一个获取博客列表接口 ,并在BlogArticleServices实现该接口Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

   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;        }    }

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

2、在API层中添加对该接口引用

(注意RESTful接口路径命名规范,我这么写只是为了测试)

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

      /// <summary>        /// 获取博客列表        /// </summary>        /// <returns></returns>        [HttpGet]        [Route("GetBlogs")]        public async Task<List<BlogArticle>> GetBlogs()        {            return await blogArticleServices.getBlogs();        }

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

3、添加AOP拦截器

在Blog.Core新建文件夹AOP,并添加拦截器BlogLogAOP,并设计其中用到的日志记录Logger方法或者类

 Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

关键的一些知识点,注释中已经说明了,主要是有以下:
1、继承接口IInterceptor
2、实例化接口IINterceptor的唯一方法Intercept
3、void Proceed();表示执行当前的方法和object ReturnValue { get; set; }执行后调用,object[] Arguments参数对象
4、中间的代码是新建一个类,还是单写,就很随意了。

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

  /// <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        }    }

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存 

提示:这里展示了如何在项目中使用AOP实现对 service 层进行日志记录,如果你想实现异常信息记录的话,很简单,

注意,这个方法仅仅是针对同步的策略,如果你的service是异步的,这里获取不到,正确的写法,在文章底部的 GitHub 代码里,因为和 AOP 思想没有直接的关系,这里就不赘述。

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存 下边的是完整代码:Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || 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();}

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存 

4、添加到Autofac容器中,实现注入

 还记得昨天的容器么,先把拦截器注入,然后对程序集的注入方法中添加拦截器服务即可 Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

        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));//可以直接替换拦截器

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存 

注意其中的两个方法.EnableInterfaceInterceptors()//对目标类型启用接口拦截。拦截器将被确定,通过在类或接口上截取属性, 或添加 InterceptedBy ().InterceptedBy(typeof(BlogLogAOP));//允许将拦截器服务的列表分配给注册。说人话就是,将拦截器添加到要注入容器的接口或者类之上。

 

5、运行项目,查看效果

嗯,你就看到这根目录下生成了一个Log文件夹,里边有日志记录,当然记录很简陋,里边是获取到的实体类,大家可以自己根据需要扩展Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

这里,面向服务层的日志记录就完成了,大家感觉是不是很平时的不一样?

 

二、AOP 之 实现接口数据的缓存功能

想一想,如果我们要实现缓存功能,一般咱们都是将数据获取到以后,定义缓存,然后在其他地方使用的时候,在根据key去获取当前数据,然后再操作等等,平时都是在API接口层获取数据后进行缓存,今天咱们可以试试,在接口之前就缓存下来。 

1、定义 Memory 缓存类和接口

老规矩,定义一个缓存类和接口,你会问了,为什么上边的日志没有定义,因为我会在之后讲Redis的时候用到这个缓存接口Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

   /// <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));        }    }

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存 

2、定义一个缓存拦截器

还是继承IInterceptor,并实现InterceptZ从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

   /// <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 "";        }    }

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

注释的很清楚,基本都是情况

 

3、注入缓存拦截器

ConfigureServices不用动,只需要改下拦截器的名字就行

注意:

//将 TService 中指定的类型的范围服务添加到实现
services.AddScoped<ICaching, MemoryCaching>();//记得把缓存注入!!!

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

4、运行,查看效果

你会发现,首次缓存是空的,然后将Repository仓储中取出来的数据存入缓存,第二次使用就是有值了,其他所有的地方使用,都不用再写了,而且也是面向整个程序集合的Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

5、多个AOP执行顺序问题

在我最新的 Github 项目中,我定义了三个 AOP :除了上边两个 LogAOP和 CacheAOP 以外,还有一个 RedisCacheAOP,并且通过开关的形式在项目中配置是否启用:

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

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

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

6、无接口如何实现AOP

上边我们讨论了很多,但是都是接口框架的,

比如:Service.dll 和与之对应的 IService.dll,Repository.dll和与之对应的 IRepository.dll,我们可以直接在对应的层注入的时候,匹配上 AOP 信息,但是如果我们没有使用接口怎么办?

这里大家可以安装下边的实验下:

Autofac它只对接口方法 或者 虚virtual方法或者重写方法override才能起拦截作用。

如果没有接口

案例是这样的:

如果我们的项目是这样的,没有接口,会怎么办:
Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存 

    // 服务层类   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;    }

 Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

如果是没有接口的单独实体类

Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存 

    public class Love    {        // 一定要是虚方法        public virtual string SayLoveU()        {            return "I ♥ U";        }    }//---------------------------//只能注入该类中的虚方法builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love)))    .EnableClassInterceptors()    .InterceptedBy(typeof(BlogLogAOP));

 Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存 

三、还有其他的一些问题需要考虑

1、可以针对某一层的指定类的指定方法进行操作,这里就不写了,大家可以自己实验

配合Attribute就可以只拦截相应的方法了。因为拦截器里面是根据Attribute进行相应判断的!!builder.RegisterAssemblyTypes(assembly)   .Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract) .AsImplementedInterfaces()   .InstancePerLifetimeScope()   .EnableInterfaceInterceptors()   .InterceptedBy(typeof(QCachingInterceptor));

2、时间问题,阻塞,浪费资源问题等  定义切面有时候是方便,初次使用会很别扭,使用多了,可能会对性能有些许的影响,因为会大量动态生成代理类,性能损耗,是特别高的请求并发,比如万级每秒,还是不建议生产环节推荐。所以说切面编程要深入的研究,不可随意使用,我说的也是九牛一毛,大家继续加油吧! 3、静态注入

基于Net的IL语言层级进行注入,性能损耗可以忽略不计,Net使用最多的Aop框架PostSharp(好像收费了;)采用的即是这种方式。

大家可以参考这个博文:https://www.cnblogs.com/mushroom/p/3932698.html

四、结语

  今天的讲解就到了这里了,通过这两个小栗子,大家应该能对面向切面编程有一些朦胧的感觉了吧,感兴趣的可以深入的研究,也欢迎一起讨论,刚刚在缓存中,我说到了缓存接口,就引入了下次的讲解内容,Redis的高性能缓存框架,内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。下次再见咯~

1、网友好资料

  1. 带你学习AOP框架之Aspect.Core[1]

五、Github && Gitee

https://github.com/anjoy8/Blog.Core

https://gitee.com/laozhangIsPhi/Blog.Core

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:8,954
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,479
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,291
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,108
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,740
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,774