在ASP.NET Core中使用AOP来简化缓存操作

前言

关于缓存的使用,相信大家都是熟悉的不能再熟悉了,简单来说就是下面一句话。

优先从缓存中取数据,缓存中取不到再去数据库中取,取到了在扔进缓存中去。

然后我们就会看到项目中有类似这样的代码了。

public Product Get(int productId){  
 var product = _cache.Get($"Product_{productId}");    if(product == null){product = Query(productId);_cache.Set($"Product_{productId}",product ,10);}    return product; }

然而在初期,没有缓存的时候,可能这个方法就一行代码。

public Product Get(int productId){    return Query(productId);
}

随着业务的不断发展,可能会出现越来越多类似第一段的示例代码。这样就会出现大量“重复的代码”了!

显然,我们不想让这样的代码到处都是!

基于这样的情景下,我们完全可以使用AOP去简化缓存这一部分的代码。

大致的思路如下 :

在某个有返回值的方法执行前去判断缓存中有没有数据,有就直接返回了;

如果缓存中没有的话,就是去执行这个方法,拿到返回值,执行完成之后,把对应的数据写到缓存中去,

下面就根据这个思路来实现。

本文分别使用了Castle和AspectCore来进行演示。

这里主要是做了做了两件事

  1. 自动处理缓存的key,避免硬编码带来的坑

  2. 通过Attribute来简化缓存操作

下面就先从Castle开始吧!

使用Castle来实现

一般情况下,我都会配合Autofac来实现,所以这里也不例外。

我们先新建一个ASP.NET Core 2.0的项目,通过Nuget添加下面几个包(当然也可以直接编辑csproj来完成的)。

<PackageReference Include="Autofac" Version="4.6.2" /><PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.0" /><PackageReference Include="Autofac.Extras.DynamicProxy" Version="4.2.1" /><PackageReference Include="Castle.Core" Version="4.2.1" />

然后做一下前期准备工作

1.缓存的使用

定义一个ICachingProvider和其对应的实现类MemoryCachingProvider

简化了一下定义,就留下读和取的操作。

public interface ICachingProvider{   

 object Get(string cacheKey);  
  void Set(string cacheKey, object cacheValue, TimeSpan absoluteExpirationRelativeToNow); }
  public class MemoryCachingProvider : ICachingProvider{  
   private IMemoryCache _cache;  
  
   public MemoryCachingProvider(IMemoryCache cache)    {_cache = cache;}  
   
    public object Get(string cacheKey)    {        return _cache.Get(cacheKey);}  
    
    public void Set(string cacheKey, object cacheValue, TimeSpan absoluteExpirationRelativeToNow)    {_cache.Set(cacheKey, cacheValue, absoluteExpirationRelativeToNow);} }

2.定义一个Attribute

这个Attribute就是我们使用时候的关键了,把它添加到要缓存数据的方法中,即可完成缓存的操作。

这里只用了一个绝对过期时间(单位是秒)来作为演示。如果有其他缓存的配置,也是可以往这里加的。

[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class QCachingAttribute : Attribute{  
    public int AbsoluteExpiration { get; set; } = 30;    //add other settings ...}

3.定义一个空接口

这个空接口只是为了做一个标识的作用,为了后面注册类型而专门定义的。

public interface IQCaching{
}

4.定义一个与缓存键相关的接口

定义这个接口是针对在方法中使用了自定义类的时候,识别出这个类对应的缓存键。

public interface IQCachable{    string CacheKey { get; }
}

准备工作就这4步(AspectCore中也是要用到的),

下面我们就是要去做方法的拦截了(拦截器)。

拦截器首先要继承并实现IInterceptor这个接口。

public class QCachingInterceptor : IInterceptor{  

 private ICachingProvider _cacheProvider;  
 
   public QCachingInterceptor(ICachingProvider cacheProvider)    {_cacheProvider = cacheProvider;}  
   
   public void Intercept(IInvocation invocation)    {    
       var qCachingAttribute = this.GetQCachingAttributeInfo(invocation.MethodInvocationTarget ?? invocation.Method);      
       if (qCachingAttribute != null){ProceedCaching(invocation, qCachingAttribute);}      
       else{invocation.Proceed();}} }

有两点要注意:

  1. 因为要使用缓存,所以这里需要我们前面定义的缓存操作接口,并且在构造函数中进行注入。

  2. Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义。

Intercept方法其实很简单,获取一下当前执行方法是不是有我们前面自定义的QCachingAttribute,有的话就去处理缓存,没有的话就是仅执行这个方法而已。

下面揭开ProceedCaching方法的面纱。

private void ProceedCaching(IInvocation invocation, QCachingAttribute attribute){    var cacheKey = GenerateCacheKey(invocation);   
 var cacheValue = _cacheProvider.Get(cacheKey);  
   if (cacheValue != null){invocation.ReturnValue = cacheValue;    
       return;}invocation.Proceed();  
    if (!string.IsNullOrWhiteSpace(cacheKey)){_cacheProvider.Set(cacheKey, invocation.ReturnValue, TimeSpan.FromSeconds(attribute.AbsoluteExpiration));} }

这个方法,就是和大部分操作缓存的代码一样的写法了!

注意下面几个地方

  1. invocation.Proceed()表示执行当前的方法

  2. invocation.ReturnValue是要执行后才会有值的。

  3. 在每次执行前,都会依据当前执行的方法去生成一个缓存的键。

下面来看看生成缓存键的操作。

这里生成的依据是当前执行方法的名称,参数以及该方法所在的类名。

生成的代码如下:

private string GenerateCacheKey(IInvocation invocation){   
 var typeName = invocation.TargetType.Name;  
  var methodName = invocation.Method.Name;  
   var methodArguments = this.FormatArgumentsToPartOfCacheKey(invocation.Arguments);  
     return this.GenerateCacheKey(typeName, methodName, methodArguments); }
     //拼接缓存的键

private string GenerateCacheKey(string typeName, string methodName, IList<string> parameters){  
      var builder = new StringBuilder();builder.Append(typeName);builder.Append(_linkChar);builder.Append(methodName);builder.Append(_linkChar);  
 foreach (var param in parameters){builder.Append(param);builder.Append(_linkChar);}    return builder.ToString().TrimEnd(_linkChar); }
 
 private IList<string> FormatArgumentsToPartOfCacheKey(IList<object> methodArguments, int maxCount = 5){    
 return methodArguments.Select(this.GetArgumentValue).Take(maxCount).ToList(); }//处理方法的参数,可根据情况自行调整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");
              if (arg is IQCachable)    
                  return ((IQCachable)arg).CacheKey;  
                   return null; }

这里要注意的是GetArgumentValue这个方法,因为一个方法的参数有可能是基本的数据类型,也有可能是自己定义的类。

对于自己定义的类,必须要去实现IQCachable这个接口,并且要定义好键要取的值!

如果说,在一个方法的参数中,有一个自定义的类,但是这个类却没有实现IQCachable这个接口,那么生成的缓存键将不会包含这个参数的信息。

举个生成的例子:

MyClass:MyMethod:100:abc:999

到这里,我们缓存的拦截器就已经完成了。

下面是删除了注释的代码(可去github上查看完整的代码)

public class QCachingInterceptor : IInterceptor{    

private ICachingProvider _cacheProvider;  

 private char _linkChar = ':';  
 
  public QCachingInterceptor(ICachingProvider cacheProvider)    {_cacheProvider = cacheProvider;}  
  
   public void Intercept(IInvocation invocation)    {    
      var qCachingAttribute = this.GetQCachingAttributeInfo(invocation.MethodInvocationTarget ?? invocation.Method);    
         if (qCachingAttribute != null){ProceedCaching(invocation, qCachingAttribute);}      
        else{invocation.Proceed();}}    
        private QCachingAttribute GetQCachingAttributeInfo(MethodInfo method)    {      
          return method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(QCachingAttribute)) as QCachingAttribute;}    private void ProceedCaching(IInvocation invocation, QCachingAttribute attribute)    {      
           var cacheKey = GenerateCacheKey(invocation);  
           var cacheValue = _cacheProvider.Get(cacheKey);    
           if (cacheValue != null){invocation.ReturnValue = cacheValue;      
                return;}invocation.Proceed();    
        if (!string.IsNullOrWhiteSpace(cacheKey)){_cacheProvider.Set(cacheKey, invocation.ReturnValue, TimeSpan.FromSeconds(attribute.AbsoluteExpiration));}}  
    
     private string GenerateCacheKey(IInvocation invocation)    {        var typeName = invocation.TargetType.Name;    
         var methodName = invocation.Method.Name;    
             var methodArguments = this.FormatArgumentsToPartOfCacheKey(invocation.Arguments);    
                return this.GenerateCacheKey(typeName, methodName, methodArguments);}  
    
     private string GenerateCacheKey(string typeName, string methodName, IList<string> parameters)    {      
      var builder = new StringBuilder();builder.Append(typeName);builder.Append(_linkChar);builder.Append(methodName);builder.Append(_linkChar);      
        foreach (var param in parameters){builder.Append(param);builder.Append(_linkChar);}        return builder.ToString().TrimEnd(_linkChar);}    
        private IList<string> FormatArgumentsToPartOfCacheKey(IList<object> methodArguments, int maxCount = 5)    {    
           return methodArguments.Select(this.GetArgumentValue).Take(maxCount).ToList();}  
            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");      
                     if (arg is IQCachable)      
                         return ((IQCachable)arg).CacheKey;    
                             return null;} }  

下面就是怎么用的问题了。

这里考虑了两种用法:

  • 一种是面向接口的用法,也是目前比较流行的用法

  • 一种是传统的,类似通过实例化一个BLL层对象的方法。

先来看看面向接口的用法

public interface IDateTimeService{        string GetCurrentUtcTime();
}

public class DateTimeService : IDateTimeService, QCaching.IQCaching{[QCaching.QCaching(AbsoluteExpiration = 10)]  
 public string GetCurrentUtcTime()    {  
      return System.DateTime.UtcNow.ToString();} }

简单起见,就返回当前时间了,也是看缓存是否生效最简单有效的办法。

在控制器中,我们只需要通过构造函数的方式去注入我们上面定义的Service就可以了。

public class HomeController : Controller{   

 private IDateTimeService _dateTimeService;  
 
  public HomeController(IDateTimeService dateTimeService)    {_dateTimeService = dateTimeService;}    
  
  public IActionResult Index()    {    
     return Content(_dateTimeService.GetCurrentUtcTime());} }

如果这个时候运行,肯定是会出错的,因为我们还没有配置!

去Starpup中修改一下ConfigureServices方法,完成我们的注入和启用拦截操作。

public class Startup{  

 public IServiceProvider ConfigureServices(IServiceCollection services)    {services.AddMvc();services.AddScoped<ICachingProvider, MemoryCachingProvider>();        return this.GetAutofacServiceProvider(services);}    
 
 private IServiceProvider GetAutofacServiceProvider(IServiceCollection services)    {      
  var builder = new ContainerBuilder();builder.Populate(services);    
      var assembly = this.GetType().GetTypeInfo().Assembly;builder.RegisterType<QCachingInterceptor>();        //scenario 1builder.RegisterAssemblyTypes(assembly).Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract).AsImplementedInterfaces().InstancePerLifetimeScope().EnableInterfaceInterceptors().InterceptedBy(typeof(QCachingInterceptor));       return new AutofacServiceProvider(builder.Build());}    //other ...}

要注意的是这个方法原来是没有返回值的,现在需要调整为返回IServiceProvider

这段代码,网上其实有很多解释,这里就不再细说了,主要是EnableInterfaceInterceptorsInterceptedBy

下面是运行的效果:

再来看看通过实例化的方法

先定义一个BLL层的方法,同样是返回当前时间。这里我们直接把Attribute放到这个方法中即可,同时还要注意是virtual的。

public class DateTimeBLL : QCaching.IQCaching{[QCaching.QCaching(AbsoluteExpiration = 10)]  
 public virtual string GetCurrentUtcTime()    {    
     return System.DateTime.UtcNow.ToString();} }

在控制器中,就不是简单的实例化一下这个BLL的对象就行了,还需要借肋ILifetimeScope去Resolve。如果是直接实例化的话,是没办法拦截到的。

public class BllController : Controller{ 

   private ILifetimeScope _scope;  
    private DateTimeBLL _dateTimeBLL;  
    
     public BllController(ILifetimeScope scope)    {      
     this._scope = scope;_dateTimeBLL = _scope.Resolve<DateTimeBLL>();}  
     
      public IActionResult Index()    {  
           return Content(_dateTimeBLL.GetCurrentUtcTime());} }

同时还要在builder中启用类的拦截EnableClassInterceptors

//scenario 2builder.RegisterAssemblyTypes(assembly).Where(type => type.Name.EndsWith("BLL", StringComparison.OrdinalIgnoreCase)).EnableClassInterceptors().InterceptedBy(typeof(QCachingInterceptor));

效果如下:

到这里已经通过Castle和Autofac完成了简化缓存的操作了。

下面再来看看用AspectCore该如何来实现。

使用AspectCore来实现

AspectCore是由Lemon丶写的一个基于AOP的框架。

首先还是要通过Nuget添加一下相应的包。这里只需要添加两个就可以了。

<PackageReference Include="AspectCore.Core" Version="0.2.2" /><PackageReference Include="AspectCore.Extensions.DependencyInjection" Version="0.2.2" />

用法大同小异,所以后面只讲述一下使用上面的不同点。

注:我也是下午看了一下作者的博客和一些单元测试代码写的下面的示例代码,希望没有对大家造成误导。

首先,第一个不同点就是我们的拦截器。这里需要去继承AbstractInterceptor这个抽象类并且要去重写Invoke方法。

public class QCachingInterceptor : AbstractInterceptor{[FromContainer]   
 public ICachingProvider CacheProvider { get; set; }  
 
  public async override Task Invoke(AspectContext context, AspectDelegate next)    {        var qCachingAttribute = GetQCachingAttributeInfo(context.ServiceMethod);    
      if (qCachingAttribute != null){          
       await ProceedCaching(context, next, qCachingAttribute);}        
       else{          
        await next(context);}} }

细心的读者会发现,两者并没有太大的区别!

缓存的接口,这里是用FromContainer的形式的处理的。

接下来是Service的不同。

这里主要就是把Attribute放到了接口的方法中,而不是其实现类上面。

public interface IDateTimeService : QCaching.IQCaching{     [QCaching.QCaching(AbsoluteExpiration = 10)]  
 string GetCurrentUtcTime(); }
 public class DateTimeService : IDateTimeService{    //[QCaching.QCaching(AbsoluteExpiration = 10)]public string GetCurrentUtcTime()    {  
       return System.DateTime.UtcNow.ToString();} }

然后是使用实例化方式时的控制器也略有不同,主要是替换了一下相关的接口,这里用的是IServiceResolver

public class BllController : Controller{   

 private IServiceResolver _scope;    
 private DateTimeBLL _dateTimeBLL;  
   public BllController(IServiceResolver scope)    {    
       this._scope = scope;_dateTimeBLL = _scope.Resolve<DateTimeBLL>();}  
        public IActionResult Index()    {    
            return Content(_dateTimeBLL.GetCurrentUtcTime());}

最后,也是至关重要的Stratup。

public class Startup{   

 public IServiceProvider ConfigureServices(IServiceCollection services)    {services.AddMvc();services.AddScoped<ICachingProvider, MemoryCachingProvider>();services.AddScoped<IDateTimeService, DateTimeService>();        //handle BLL classvar assembly = this.GetType().GetTypeInfo().Assembly;        this.AddBLLClassToServices(assembly, services);      
  var container = services.ToServiceContainer();container.AddType<QCachingInterceptor>();container.Configure(config =>{config.Interceptors.AddTyped<QCachingInterceptor>(method => typeof(IQCaching).IsAssignableFrom(method.DeclaringType));});        return container.Build();}    
  
  public void AddBLLClassToServices(Assembly assembly, IServiceCollection services)    {    
     var types = assembly.GetTypes().ToList();      
      foreach (var item in types.Where(x => x.Name.EndsWith("BLL", StringComparison.OrdinalIgnoreCase) && x.IsClass)){services.AddSingleton(item);}}    //other code...}

我这里是先用自带的DependencyInjection完成了一些操作,然后才去用ToServiceContainer()得到AspectCore内置容器。

得到这个容器后,就去配置拦截了。

最终的效果是和前面一样的,就不再放图了。

总结

AOP在某些方面的作用确实很明显,也很方便,能做的事情也很多。

对比Castle和AspectCore的话,两者各有优点!

就我个人使用而言,对Castle略微熟悉一下,资料也比较多。

对AspectCore的话,我比较喜欢它的配置,比较简单,依赖也少。

原文地址:http://www.cnblogs.com/catcher1994/p/7788890.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/323003.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Hadoop入门(七)Mapreduce高级Shuffle

一、Shuffle概述 Reduce阶段三个步骤&#xff0c;Shuffle就是一个随机、洗牌操作 Shuffle是什么 针对多个map任务的输出按照不同的分区&#xff08;Partition&#xff09;通过网络复制到不同的reduce任务节点上&#xff0c;这个过程就称作为Shuffle。 二、Shuffle过程 &#…

methods中axios里的数据无法渲染到页面

最近在研究axios聊天室室遇到一个问题 将axios获取到的数据传递给data&#xff0c;从而改变页面中的数值&#xff0c;但是结果令人失望 这是data里的数据 原想将data中的items数组换成axios里的response.data&#xff0c;后来发现items一直为空&#xff0c;就拿字符串做实验了…

.NET Core跨平台的奥秘[上篇]:历史的枷锁

微软推出的第一个版本的.NET Framework是一个面向Windows桌面和服务器的基础框架&#xff0c;在此之后&#xff0c;为此微软根据设备自身的需求对.NET Framework进行裁剪&#xff0c;不断推出了针对具体设备类型的.NET Framework版本以实现针对移动、平板和嵌入式设备提供支持。…

Hadoop入门(十)Mapreduce高级shuffle之Sort和Group

一、排序分组概述 MapReduce中排序和分组在哪里被执行 第3步中需要对不同分区中的数据进行排序和分组&#xff0c;默认情况按照key进行排序和分组 二、排序 在Hadoop默认的排序算法中&#xff0c;只会针对key值进行排序 任务&#xff1a; 数据文件中&#xff0c;如果按照第一…

使用Identity Server 4建立Authorization Server (3)

预备知识: 学习Identity Server 4的预备知识 第一部分: 使用Identity Server 4建立Authorization Server (1) 第二部分: 使用Identity Server 4建立Authorization Server (2) 上一部分简单的弄了个web api 并通过Client_Credentials和ResourceOwnerPassword两种方式获取token然…

php接口跨域问题

报错是因为接口跨域&#xff0c;不允许访问 只需在php头部加入此行代码就行了 header(Access-Control-Allow-Origin:*);

spring boot输出hello world几种方法

1、手动配置&#xff0c;三个文件 打开创建maven,创建这三个文件从上到下依次复制即可 配置文件&#xff08;重要&#xff09;&#xff08;否则后面会报错&#xff09; pom.xml <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w…

欢乐纪中某B组赛【2019.1.20】

前言 有回来做BBB组了&#xff0c;话说第3道题就是AAA组第一道。 成绩 RankRankRank是有算别人的 今天XJQXJQXJQ不在 RankRankRankPersonPersonPersonScoreScoreScoreAAABBBCCC1112017wyc2017wyc2017wyc2702702701001001001001001007070701010102017hjq2017hjq2017hjq13013013…

向ASP.NET Core迁移

我们首先来看看ASP.NET Core有哪些优势&#xff1f; 跨平台&#xff1a;可以部署到Linux服务器上 内置一套对云和部署环境非常友好的配置模块 内置依赖注入 IIS或者Kestrel&#xff08;或者其它自定义&#xff09; 轻量级、高性能、模块化的Http处理管线 .NET Core 是开源…

ASP.NET Core集成现有系统认证

我们现在大多数转向ASP.NET Core来使用开发的团队&#xff0c;应该都不是从0开始搭建系统&#xff0c;而是老的业务系统已经在运行&#xff0c;ASP.NET Core用来开发新模块。那么解决用户认证的问题&#xff0c;成为我们的第一个拦路虎。 认证与授权 什么是认证&#xff1f; …

使用Identity Server 4建立Authorization Server (4)

预备知识: 学习Identity Server 4的预备知识 第一部分: 使用Identity Server 4建立Authorization Server (1) 第二部分: 使用Identity Server 4建立Authorization Server (2) 第三部分: 使用Identity Server 4建立Authorization Server (3) 上一篇讲了使用OpenId Connect进行Au…

Hadoop入门(十八)Mapreduce的倒排索引程序

一、简介 "倒排索引"是文档检索系统中最常用的数据结构&#xff0c;被广泛地应用于全文搜索引擎。它主要是用来存储某个单词&#xff08;或词组&#xff09;在一个文档或一组文档中的存储位置的映射&#xff0c;即提供了一种根据内容来查找文档的方式。由于不是根据…

.NET Core跨平台的奥秘[中篇]:复用之殇

在《.NET Core跨平台的奥秘[上篇]&#xff1a;历史的枷锁》中我们谈到&#xff1a;由于.NET是建立在CLI这一标准的规范之上&#xff0c;所以它天生就具有了“跨平台”的基因。在微软发布了第一个针对桌面和服务器平台的.NET Framework之后&#xff0c;它开始 “乐此不疲” 地对…

Hadoop入门(十一)Mapreduce的InputFomrat各种子类

一、TextInputFormat extends FileInputFomrat<LongWritable,Text> 是默认读取文件的切分器&#xff0c;其内的LineRecordReader:用来读取每一行的内容&#xff0c; LineRecordReader:内的 nextKeyValue(){}中&#xff0c;key的赋值在&#xff1a; initialize()方法内&…

极简版ASP.NET Core学习路径

拒绝承认这是一个七天速成教程&#xff0c;即使有这个效果&#xff0c;我也不愿意接受这个名字。嗯。 这个路径分为两块&#xff1a; 实践入门 理论延伸 有了ASP.NET以及C#的知识以及项目经验&#xff0c;我们几乎可以不再需要了解任何新的知识就开始操练&#xff0c;实践才是…

依存句法分析的任务以及形式化定义

转载自 依存句法分析的任务以及形式化定义 依存句法分析的任务以及形式化定义 1、依存句法分析的形式化定义 在依存句法中&#xff0c;共同的基本假设是&#xff1a;句法结构本质上包含词和词对之间的关系。这种关系就是依存关系&#xff08;dependency relations&#xff…

使用Identity Server 4建立Authorization Server (5)

预备知识: 学习Identity Server 4的预备知识 第一部分: 使用Identity Server 4建立Authorization Server (1) 第二部分: 使用Identity Server 4建立Authorization Server (2) 第三部分: 使用Identity Server 4建立Authorization Server (3) 第四部分: 使用Identity Server 4建立…

好好说说Java中的常量池之Class常量池

转载自 好好说说Java中的常量池之Class常量池 在Java中&#xff0c;常量池的概念想必很多人都听说过。这也是面试中比较常考的题目之一。在Java有关的面试题中&#xff0c;一般习惯通过String的有关问题来考察面试者对于常量池的知识的理解&#xff0c;几道简单的String面试…

spring cloud+.net core搭建微服务架构:Api授权认证(六)

前言 这篇文章拖太久了&#xff0c;因为最近实在太忙了&#xff0c;加上这篇文章也非常长&#xff0c;所以花了不少时间&#xff0c;给大家说句抱歉。好&#xff0c;进入正题。目前的项目基本都是前后端分离了&#xff0c;前端分Web&#xff0c;Ios,Android。。。,后端也基本是…

如何用spring boot写一个注册页面

环境准备&#xff1a; java集成开发环境&#xff1a;IDEA 数据库&#xff1a;Mysql Maven 最好在安装有个navicat&#xff08;数据库可视化界面&#xff09; 安装好上述几个软件后 总结下&#xff1a;五步 1、创建新的工程 2、创建建applicatiom.yml 3、创建entity层 4、创建r…