2024年7月

Quartz.NET作为一个开源的作业调度库,广泛应用于.NET应用程序中,以实现复杂的定时任务,本次记录利用Quartz.NET实现HTTP作业调度,通过自定义HTTP作业,实现对外部API的定时调用和如何管理这些作业,包括创建、修改、暂停、恢复和删除作业。

1.首先定义了一个
HttpJob
类,该类实现了
IJob
接口,用于执行HTTP请求。利用了
RestRequest
来构建请求,并通过静态字典
Delegates
存储每个作业的配置信息,如URL、请求方法和请求头等

public classHttpJob : IJob
{
public static readonly Dictionary<string, HttpJobInfo> Delegates = new();public asyncTask Execute(IJobExecutionContext context)
{
var delegateKey = context.JobDetail.JobDataMap.GetString("delegateKey");if (delegateKey != null && Delegates.TryGetValue(delegateKey, out varfunc))
{
var requestBody = newRestRequest();if (func.Headers != null)
{
foreach (var header infunc.Headers)
{
requestBody.AddHeader(header.Key, header.Value);
}
}
var content =HttpHelper.HttpRequest(func.Url, func.Request, requestBody);
JobLogHelper.AddJobLog(
new JobLog() { JobName = context.JobDetail.Key.Name, GroupName = context.JobDetail.Key.Group, RunTime = DateTime.Now, RunResult =content });
UpdateLastExecutionTime(context.JobDetail.Key.Name, context.JobDetail.Key.Group, DateTime.Now);
}
awaitTask.CompletedTask;
}
}

2.作业信息的持久化:为了持久化作业信息,定义了
JobInfo
类来存储作业的基本信息,如名称、组名、Cron表达式等,并将这些信息保存在本地的JSON文件中。

public classJobInfo
{
public required string JobName { get; set; }public required string GroupName { get; set; }public required string CronExpression { get; set; }public DateTime LastExecutionTime { get; set; }public JobStatus Status { get; set; }public required HttpJobInfo HttpJob { get; set; }
}

3.实现了
QuartzHelper
类,用于管理作业的生命周期。这包括加载作业信息、创建作业、调度作业、暂停/恢复作业以及删除作业等功能。


 public classQuartzHelper
{
privateIScheduler scheduler;private List<JobInfo>jobInfos;private string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "jobs.json");/// <summary> ///构造函数,初始化定时任务管理器/// </summary> publicQuartzHelper()
{
ISchedulerFactory schedulerFactory
= newStdSchedulerFactory();
scheduler
=schedulerFactory.GetScheduler().Result;
scheduler.Start().Wait();
LoadJobInfosApi().Wait();

}
/// <summary> ///保存作业信息到本地 JSON 文件/// </summary> private voidSaveJobInfos()
{
string json =JsonConvert.SerializeObject(jobInfos);
File.WriteAllText(filePath, json);
}
/// <summary> ///加载本地 JSON 文件中的作业信息/// </summary> private asyncTask LoadJobInfosApi()
{
if(File.Exists(filePath))
{
string json =File.ReadAllText(filePath);
jobInfos
= JsonConvert.DeserializeObject<List<JobInfo>>(json) ?? new List<JobInfo>();foreach (var jobInfo injobInfos)
{
//创建委托的唯一键 var delegateKey =Guid.NewGuid().ToString();//将委托存储在静态字典中 HttpJob.Delegates[delegateKey] =jobInfo.HttpJob;//创建并调度作业 IJobDetail job = JobBuilder.Create<HttpJob>()
.WithIdentity(jobInfo.JobName, jobInfo.GroupName).UsingJobData(
"delegateKey", delegateKey) //将委托的键添加到JobDataMap .Build();

ITrigger trigger
=TriggerBuilder.Create()
.WithIdentity(jobInfo.JobName, jobInfo.GroupName)
.WithCronSchedule(jobInfo.CronExpression)
//.StartNow() .Build();awaitscheduler.ScheduleJob(job, trigger);//根据任务状态恢复或暂停任务 if (jobInfo.Status ==JobStatus.正常运行)
{
awaitResumeJob(jobInfo.JobName, jobInfo.GroupName);
}
else{awaitPauseJob(jobInfo.JobName, jobInfo.GroupName);
}
}
}
else{
jobInfos
= new List<JobInfo>();
}
}
#region 执行普通任务时使用,传委托时可以参考此方法 /////<summary> ///// 新建任务并立即执行/////</summary> //[Obsolete("执行普通任务时使用,可以传委托使用")]//public async Task AddJob(string jobName, string groupName, string cronExpression, Func<bool> func, string description = "")//{//if (jobInfos.Any(c => c.JobName == jobName && c.GroupName == groupName))//{//return;//}// //创建委托的唯一键//var delegateKey = Guid.NewGuid().ToString();// //将委托存储在静态字典中// //MyJobClass.Delegates[delegateKey] = func;// //创建作业信息并保存到列表 需要将func 加入到jobInfo 中做作业持久化!!!!//var jobInfo = new JobInfo { JobName = jobName, GroupName = groupName, CronExpression = cronExpression, Status = JobStatus.正常运行, Description = description, JobCreateTime = DateTime.Now };//jobInfos.Add(jobInfo);//SaveJobInfos();// //创建Quartz作业和触发器//IJobDetail job = JobBuilder.Create<MyJobClass>()//.WithIdentity(jobName, groupName)//.UsingJobData("delegateKey", delegateKey)//将委托的键添加到JobDataMap//.Build();//ITrigger trigger = TriggerBuilder.Create()//.WithIdentity(jobName + "Trigger", groupName)//.StartNow()//.WithCronSchedule(cronExpression).WithDescription(description)//.Build();//await scheduler.ScheduleJob(job, trigger);//} #endregion /// <summary> ///新建任务并立即执行/// </summary> public async Task AddJobApi(string jobName, string groupName, string cronExpression, HttpJobInfo httpJobInfo, string description = "")
{
if (jobInfos.Any(c => c.JobName == jobName && c.GroupName ==groupName))
{
return;
}
//创建委托的唯一键 var delegateKey =Guid.NewGuid().ToString();//将委托存储在静态字典中 HttpJob.Delegates[delegateKey] =httpJobInfo;//创建作业信息并保存到列表 需要将func 加入到jobInfo 中做作业持久化!!!! var jobInfo = new JobInfo { JobName = jobName, GroupName = groupName, CronExpression = cronExpression, HttpJob = httpJobInfo, Status = JobStatus.正常运行, Description = description, JobCreateTime =DateTime.Now };
jobInfos.Add(jobInfo);
SaveJobInfos();
//创建Quartz作业和触发器 IJobDetail job = JobBuilder.Create<HttpJob>()
.WithIdentity(jobName, groupName)
.UsingJobData(
"delegateKey", delegateKey) //将委托的键添加到JobDataMap .Build();

ITrigger trigger
=TriggerBuilder.Create()
.WithIdentity(jobName
+ "Trigger", groupName)
.StartNow()
.WithCronSchedule(cronExpression).WithDescription(description)
.Build();
awaitscheduler.ScheduleJob(job, trigger);

}
/// <summary> ///暂停任务/// </summary> public async Task PauseJob(string jobName, stringgroupName)
{
await scheduler.PauseJob(newJobKey(jobName, groupName));var job = jobInfos.FirstOrDefault(j => j.JobName == jobName && j.GroupName ==groupName);if (job != null)
{
job.Status
=JobStatus.暂停;
SaveJobInfos();
}
}
/// <summary> ///开启任务/// </summary> public async Task ResumeJob(string jobName, stringgroupName)
{
await scheduler.ResumeJob(newJobKey(jobName, groupName));var job = jobInfos.FirstOrDefault(j => j.JobName == jobName && j.GroupName ==groupName);if (job != null)
{
job.Status
=JobStatus.正常运行;
SaveJobInfos();
}
}
/// <summary> ///立即执行任务/// </summary> public async Task TriggerJob(string jobName, stringgroupName)
{
await scheduler.TriggerJob(newJobKey(jobName, groupName));var job = jobInfos.FirstOrDefault(j => j.JobName == jobName && j.GroupName ==groupName);if (job != null)
{
job.LastExecutionTime
=DateTime.Now;
SaveJobInfos();
}
}
/// <summary> ///修改任务/// </summary> public async Task ModifyJob(string jobName, string groupName, string cronExpression, HttpJobInfo httpJobInfo, string description = "")
{
awaitDeleteJob(jobName, groupName);awaitAddJobApi(jobName, groupName, cronExpression, httpJobInfo, description);
}
/// <summary> ///删除任务/// </summary> public async Task DeleteJob(string jobName, stringgroupName)
{
await scheduler.DeleteJob(newJobKey(jobName, groupName));
jobInfos.RemoveAll(j
=> j.JobName == jobName && j.GroupName ==groupName);
SaveJobInfos();
}
/// <summary> ///获取当前所有任务列表/// </summary> public List<JobInfo>GetAllJobs()
{
if(File.Exists(filePath))
{
string json =File.ReadAllText(filePath);
jobInfos
= JsonConvert.DeserializeObject<List<JobInfo>>(json) ?? new List<JobInfo>();returnjobInfos;
}
else return null;

}


}

QuartzHelper

4.为了跟踪作业的执行情况,设计了
JobLog
类和
JobLogHelper
类,用于记录和查询作业执行日志。


public classJobLogHelper
{
private static string_filePath;/// <summary> ///根据作业名称和组名称获取当日的作业执行日志/// </summary> /// <param name="jobName"></param> /// <param name="groupName"></param> /// <returns></returns> public static List<JobLog> GetJobLog(string jobName, stringgroupName)
{
_filePath
= Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"jobsLog-{DateTime.Now:yyyyMMdd}.json");//检查文件是否存在 if (!File.Exists(_filePath))
{
return new List<JobLog>();
}
var jsonText = $"[{File.ReadAllText(_filePath)}]";var list = JsonConvert.DeserializeObject<List<JobLog>>(jsonText);if (list != null)
{
var result = list.Where(c => c.JobName == jobName && groupName == c.GroupName).OrderByDescending(c =>c.RunTime).ToList();returnresult;
}
return null;
}
/// <summary> ///获取所有的 作业执行日志 //可以从这里拓展其他查询条件/// </summary> /// <returns></returns> public static List<JobLog>GetAllLogs()
{
List
<JobLog> jobLogs = new List<JobLog>();var logFilePaths = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "jobsLog-*.json");
logFilePaths.ToList().ForEach(c
=>{var jsonText = $"[{File.ReadAllText(_filePath)}]";var list = JsonConvert.DeserializeObject<List<JobLog>>(jsonText);if (list != null) jobLogs.AddRange(list);
});
returnjobLogs;
}
/// <summary> ///添加作业执行日志/// </summary> /// <param name="jobLog"></param> public static voidAddJobLog(JobLog jobLog)
{
_filePath
= Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"jobsLog-{DateTime.Now:yyyyMMdd}.json");string json = JsonConvert.SerializeObject(jobLog) + ",\n";
File.AppendAllText(_filePath, json);
}
}

作业执行日志

5.最后,通过ASP.NET Core的Controller提供了一系列Web API接口,以便于通过HTTP请求管理作业。这些接口包括获取作业列表、添加作业、修改作业、删除作业、暂停作业、恢复作业和立即执行作业等。


 [Route("api/[controller]")]
[ApiController]
public classQuartzController : ControllerBase
{
private readonlyQuartzHelper _quartzHelper;publicQuartzController(QuartzHelper quartzHelper)
{
_quartzHelper
=quartzHelper;
}

[HttpGet]
[Route(
"job/GetJobs")]public objectGetJobs()
{
return Ok(new {code=200,data =_quartzHelper.GetAllJobs() });
}

[HttpGet]
[Route(
"job/GetJobLog")]public object GetJobLog(string jobName, stringgroupName)
{
return Ok(new { code = 200, data =JobLogHelper.GetJobLog(jobName, groupName) });
}
[HttpGet]
[Route(
"job/GetJobLogs")]public objectGetJobLogs()
{
return Ok(new { code = 200, data =JobLogHelper.GetAllLogs() });
}


[HttpPost]
[Route(
"job/AddJob")]public async Task<object>Add(JobInfo jobInfo)
{
try{await_quartzHelper.AddJobApi(jobInfo.JobName, jobInfo.GroupName, jobInfo.CronExpression, jobInfo.HttpJob, jobInfo.Description);return Ok(new { code = 200, msg = "创建成功!"});
}
catch(Exception ex)
{
return Ok(new { code = 500, msg =ex.Message });
}
}

[HttpPost]
[Route(
"job/ModifyJob")]public async Task<object>Edit(JobInfo jobInfo)
{
try{await_quartzHelper.ModifyJob(jobInfo.JobName, jobInfo.GroupName, jobInfo.CronExpression, jobInfo.HttpJob, jobInfo.Description);return Ok(new { code = 200, msg = "修改成功!"});
}
catch(Exception ex)
{
return Ok(new { code = 500, msg =ex.Message });
}
}

[HttpGet]
[Route(
"job/DeleteJob")]public async Task<object> Delete(string jobName, stringgroupName)
{
try{await_quartzHelper.DeleteJob(jobName, groupName);return Ok(new { code = 200, msg = "删除成功!"});
}
catch(Exception ex)
{
return Ok(new { code = 500, msg =ex.Message });
}
}

[HttpGet]
[Route(
"job/PauseJob")]public async Task<object> PauseJob(string jobName, stringgroupName)
{
try{await_quartzHelper.PauseJob(jobName, groupName);return Ok(new { code = 200, msg = "暂停成功!"});
}
catch(Exception ex)
{
return Ok(new { code = 500, msg =ex.Message });
}
}

[HttpGet]
[Route(
"job/ResumeJob")]public async Task<object> ResumeJob(string jobName, stringgroupName)
{
try{await_quartzHelper.ResumeJob(jobName, groupName);return Ok(new { code = 200, msg = "开启任务成功!"});
}
catch(Exception ex)
{
return Ok(new { code = 500, msg =ex.Message });
}
}
[HttpGet]
[Route(
"job/TriggerJob")]public async Task<object> TriggerJob(string jobName, stringgroupName)
{
try{await_quartzHelper.TriggerJob(jobName, groupName);return Ok(new { code = 200, msg = "立即执行任务命令已执行!"});
}
catch(Exception ex)
{
return Ok(new { code = 500, msg =ex.Message });
}
}
}

Web API接口

源码地址:https://github.com/yycb1994/Quartz.Net

前言

最近在看回JavaScript的面试题,this 指向问题是入坑前端必须了解的知识点,现在迎来了ES6+的时代,因为箭头函数的出现,所以感觉有必要对 this 问题梳理一下,所以刚好总结一下JavaScript中this指向的问题。

什么是JavaScript

在了解this指向的问题前,首先得了解一下什么是JavaScript。

JavaScript(简称“JS”)是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。JavaScript基于原型编程、多范式的动态脚本语言,并且支持面向对象、命令式、声明式、函数式编程范式、支持函数式编程、闭包、基于原型的继承等高级功能。

什么是this

面向对象语言中 this 表示当前对象的一个引用。

但在 JavaScript 中 this 不是固定不变的,它会随着执行环境的改变而改变。

在方法中,this 表示该方法所属的对象。

如果单独使用,this 表示全局对象。

在函数中,this 表示全局对象。

在函数中,在严格模式下,this 是未定义的(undefined)。

在事件中,this 表示接收事件的元素。

类似 call() 和 apply() 方法可以改变 this 的指向 ,引用到任何对象。

所以this的指向完全取决于函数的调用方式。

this的指向

接下来我将在(非严格模式下)通过下面例图与例子来了解this的指向。

1.不使用new关键字,使用dot调用。

var obj = {
    name: 'bug',
    obj2: {
           name: 'bug2',
           fn: function () {
                console.log(this.name); //bug2
           }
     }
}
//此处通过obj.obj2.fn(),调用了obj中的obj2中的fn函数,此时fn函数中this的指向为dot (.) 前面的对象,即为obj2,obj2中的name即是bug2。
obj.obj2.fn();

2.使用new关键字调用。

function fn() {
  this.x = 1;
}

//此处通过new关键字生成了一个实例对象,此时的this指向了该实例对象fn
var obj = new fn();

//此时的obj的结构为{x:1},所以obj.x=1
obj.x // 1

讲到new关键字就刚好衍生出看另外一个关键点,如果我用new去创建一个实例对象,这个时候实例对象有返回值呢?

通常情况下是不应该有显式的返回值的。

但是如果当return返回的是一个对象,那么将返回该对象。

但是如果当return返回非对象类型(比如数字、字符串等),那么就不会影响到new关键字对对象的创建。

以下就用几个例子来验证一下:

①return 空对象

function fn() 
{ 
   this.name= 'bug'; 
   //此处return回了一个对象
   return {}; 
}
var obj = new fn(); 
//此时因为return回的是一个对象,所以此时的obj的结构是返回的空对象{},所以obj.name才会是undefined
console.log(obj.name); //undefined

②return一个非空对象

function fn() 
{ 
   this.name= 'bug'; 
   //此处return回了一个对象
   return {name:'bug2'}; 
}
var obj = new fn(); 
//此时因为return回的是一个非空对象,所以此时的obj的结构是返回的非空对象{name:'bug2'},所以obj.name是bug2
console.log(obj.name); //bug2

③返回数字

function fn() 
{ 
   this.name= 'bug'; 
   //此处return回了一个数字
   return 11; 
}
var obj = new fn(); 
//此时因为return回的是一个数字,所以此时返回的实例对象不受影响,结构是{name:'bug'},所以obj.name是bug
console.log(obj.name); //bug

④返回字符串

function fn() 
{ 
   this.name= 'bug'; 
   //此处return回了一个字符串
   return 'xxxxx'; 
}
var obj = new fn(); 
//此时因为return回的是一个字符串,所以此时返回的实例对象不受影响,结构是{name:'bug'},所以obj.name是bug
console.log(obj.name); //bug

既然现在进入了Es6+的时代了,就不得不讲一讲箭头函数的this指向了

1.什么是箭头函数

箭头函数是ECMAScript 6中新增的一种函数定义方式,也被称为Lambda函数。 它使用箭头(=>)符号来替代传统的function关键字,从而更简洁地定义函数,使代码更加简洁易读。

箭头函数有以下特点:

①语法简洁:箭头函数表达式的语法比普通函数更简洁,使用箭头(=>)符号来定义函数,可以省略一些不必要的语法元素,如function关键字、大括号和参数列表周围的括号(如果只有一个参数)。

②this绑定:箭头函数不绑定自己的this,它会捕获定义时所在上下文的this值,这使得在回调函数或嵌套函数中使用箭头函数时,this的指向更加明确和可预测。

③没有arguments对象:箭头函数没有自己的arguments对象,这意味着它们无法访问到传统函数的特殊属性arguments。

④不能用作构造器:箭头函数不能作为构造器使用,即它们不能用作类的实例化。

2.箭头函数的this指向

因为箭头函数不绑定自己的this,它会捕获定义时所在上下文的this值。所以简单的说就是箭头函数没有属于自己的this。

一下用个例子来简单了解。

①正常function函数

const obj={
      mythis: function(){
           console.log(this) //指向了上一级对象obj
      }
}
obj.mythis() //返回了obj对象

②箭头函数

const obj={
     mythis: ()=>{
          console.log(this) //因为箭头函数没有自己的this,所以指向的是window
     }
}
obj.mythis() //返回了window

哦这里还有一个坑,就是前面说的,this指向完全取决于函数的调用方式。

你再看看这道题最终返回的是什么?

const obj={
    mythis: function(){
       console.log(this)
    }
}
var a =obj.mythis
a()
点击查看答案与解析
//是不是有小伙伴认为这里使用的是function,所以返回的还是mythis的上一级对象obj ???
//不不不,这时候返回的是window!因为this指向完全取决于函数的调用方式
//上述例子①为何返回的是obj是因为它是直接obj.mythis()去调用,this指向是mythis的上一级对象
//但是本例子是通过减mythis直接赋值给a,此时,a 成为一个普通的函数引用,它只是 obj.mythis 的一个复制,并没有 obj 对象的上下文信息
//所以,当 a 作为一个普通函数调用时(不作为对象的方法调用),在非严格模式下,JavaScript 中的 this 默认指向全局对象 window
const obj={
    mythis: function(){
       console.log(this)
    }
}
var a =obj.mythis
a() //window

当然,this的指向除了调用的方式不同而不同的同时,也可以通过其它方式强制改变this的指向!那就是使用call、apply、bind。

什么是call、apply、bind,区别是什么?

1.什么是call?

call方法可以接受两个参数,第一个参数就是this的指向,指向xxx,第二个参数为一个参数列表。当第一个参数为null或者undefined时,this默认指向window。

function fn(...args) {
    console.log(this, args);
}
let obj = {
    name: "bug"
}

//将fn的this指向obj,并传入参数列表 1,2
fn.call(obj, 1, 2); //{name:'bug'} , [1,2]

//次数fn中的this指向为window
fn(1, 2) //window , [1,2]

//当第一个参数为null时,this指向为window
fn.call(null,[1,2]);//window , [1,2]

//当第一个参数为undefined时,this指向为window
fn.call(undefined,[1,2]);//window , [1,2]

2.什么是apply?

apply方法可以接受两个参数,第一个参数就是this的指向,指向xxx,第二个参数为一个参数数组。当第一个参数为null或者undefined时,this默认指向window。

function fn(...args) {
    console.log(this, args);
}
let obj = {
    name: "bug"
}

//将fn的this指向obj,并传入参数数组 [[1,2]]
fn.apply(obj, [1,2]); //{name:'bug'} , [[1,2]]

//次数fn中的this指向为window
fn([1,2]) //window , [[1,2]]

//当第一个参数为null时,this指向为window
fn.apply(null,[1,2]);//window ,  [[1,2]]

//当第一个参数为undefined时,this指向为window
fn.apply(undefined,[1,2]);//window , [[1,2]]

3.什么是bind?

bind方法跟call、apply十分相似,第一个参数也是this的指向,第二个参数传的也是一个参数列表,但是!这个参数列表可以分多次传入!并且改变完this的指向并不会立刻执行,而是返回一个已经永久改变this指向的函数

function fn(...args) {
     console.log(this, args);
}
let obj = {
     name: "bug"
}
const bindFn = fn.bind(obj); //this变为obj,且不会立马执行
bindFn(1, 2) //得通过调用才会执行,并传入参数列表1,2,最终this指向obj {name:'bug'}
fn(1, 2) //this执行window

4.call、apply、bind的区别是什么?

①三者都可以改变函数的 this 对象指向

②三者第一个参数都是 this 要指向的对象,如果如果没有这个参数或参数为 undefined 或 null,则默认指向全局 window

③三者都可以传参,但是 apply 是数组,而 call 是参数列表,且 apply 和 call 是一次性传入参数,而 bind 可以分为多次传入bind 是返回绑定this之后的函数,apply、call 则是立即执行

总结

简单来说,this的指向不是固定不变的,它会随着执行环境的改变而改变,具体怎么改变完全取决于函数的调用方式。

箭头函数没有属于自己的this,作为方法的箭头函数this的指向是当前的上下文。

我是刚毕业一年多的小菜鸟,上述为个人学习整理内容,水平有限,如有错误之处,望各位园友不吝赐教!如果觉得不错,请点击推荐和关注!谢谢~๑•́₃•̀๑ [鲜花][鲜花][鲜花]

1、什么是依赖注入

依赖注入(Dependency Injection,简称DI),是IOC的一种别称,用来减少对象间的依赖关系。
提起依赖注入,就少不了IOC。
IOC(Inversion of Control,控制反转)是一种设计思想,它将原本在程序中手动创建对象的控制权,交由Spring框架来管理。

IOC和DI,是同一个概念的不同角度描述。(IOC是一种思想,DI则是该思想的一种具体的技术实现方案。)
也可以这么理解:
IOC是目的(目的是创建对象),DI是手段(通过什么手段获取外部对象)。

2、依赖注入的常见实现方式

  • 构造函数注入
  • setter方法注入
  • 属性注入

2.1 构造函数注入

将各个必需的依赖全部放在带有注解构造方法的参数中,并在构造方法中完成对应变量的初始化,这种方式,就是基于构造方法的注入。

@RestController
public class UserController {
    // 构造方法注入
    private UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping("/add")
    public UserInfo add(String username, String password) {
        return userService.add(username, password);
    }
}

该方式的优点:

  • 可注入不可变对象
  • 注入对象不会被修改
  • 注入对象会被完全初始化
  • 通用性更好

该方式的缺点:

  • 当需要依赖的对象过多,构造方法会臃肿

2.2 setter方法注入

在JavaBean中,通常会通过setXXX()和getXXX()方法来访问对应属性。
这些setXXX()方法统称为setter方法,getXXX()方法统称为getter方法。
通过setter方法,可以更改相应的对象属性,通过getter方法,可以获得相应属性的状态。

所以,当前对象只要为其依赖对象所对应的属性添加setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。

@Service
public class UserService {
	
    private SmsService smsService;
    
    @Autowired  //通过setter方法实现注入
    public void setWolf3Bean(SmsService smsService) {
        this.smsService = smsService;
    }
}

该方式的优点:

  • 完全符合单一职责的设计原则,因为每一个 Setter 只针对一个对象

该方式的缺点:

  • 不能注入不可变对象(final 修饰的对象)
  • 注入的对象可被修改

2.3 属性注入

属性注入,就是在bean的变量上使用注解进行依赖注入。

属性注入是我们最熟悉的,也是日常开发中使用最多的一种注入方式,它的实现代码如下:

@RestController
public class UserController {
    // 属性对象
    @Autowired
    private UserService userService;

    @RequestMapping("/add")
    public UserInfo add(String username, String password) {
        return userService.add(username, password);
    }
}

该方式的优点:

  • 使用简单

该方式的缺点:

  • 不能注入不可变对象(final 修饰的对象)
  • 注入的对象可被修改
  • 只能适应于 IoC 容器

3、总结

在实际开发中,根据不同的场景,选择不同的注入方式。
简单来说,就是

  • 强制依赖就用构造器方式
  • 可选、可变的依赖就用setter注入

但,日常开发应该还是属性注入较多~

上篇文章“
vSAN Data Protection Part 1:安装部署。
”介绍了如何安装及部署 VMware Snapshot Service Appliance 设备,并在 vSAN ESA 集群中启用 vSAN Data Protection 功能。这篇文章继续了解 vSAN Data Protection 相关功能的配置与管理。

登录 vSphere Client 管理界面,您可以导航到 vSAN 集群-配置-vSAN-数据保护,或者在 vSAN 集群摘要“数据保护”选项卡中点击“管理”打开 vSAN Data Protection 管理视图。在“摘要”视图中显示有关保护组的常规信息,包括成员虚拟机列表、快照调度以及生成的快照数量。

一、创建保护组

使用 vSAN 数据保护,可以通过“保护组”创建基于策略的灵活保护方式。这些策略可以根据特定的业务需求进行自定义设置,比如创建保护组对 VM 虚拟机进行分组(如HR、Database、Dev),每个保护组可以分别设置保护策略和调度任务,属于 vSAN 集群的 VM 虚拟机最多可以属于 3 个保护组,每个保护组最多支持 10 个调度任务。

点击“保护组”-“创建保护组”。配置保护组的名称以及策略。

  • 不可变模式

启用不可变模式时,无法编辑或删除保护组、更改虚拟机成员资格、编辑或删除快照,同时管理员将无法禁用该模式。不可变快照是数据的只读副本,即使具有管理特权的攻击者也无法对其进行修改或删除。

  • 成员资格

定义加入保护组的虚拟机的成员资格,支持以下方式:

    • 动态虚拟机名称模式

通过通配符(如 * 或 ?)的形式动态(Dynamic)的匹配虚拟机名称并加入保护组,如 SQL-*,可以匹配多个字符;SQL-?,可以匹配单个字符。

    • 选择单个虚拟机

手动(Static)方式选择虚拟机并加入保护组。

    • 与模式匹配的虚拟机和手动选择的虚拟机将包含在保护组中

同时包含动态(Dynamic)和手动(Static)两者方式选择虚拟机并加入保护组。

选择“选择单个虚拟机”方式以手动选择虚拟机,点击下一步。选择 vSAN 集群中需要添加到保护组的虚拟机,vSAN 保护组不支持链接克隆虚拟机以及具有现有 vSphere 快照的虚拟机,这些虚拟机不会显示在选择视图中,比如当前环境中的 HCIBench 虚拟机。

设置快照调度任务。注意,生成快照间隔的时间最低只能设置为 30 分钟,保留快照的时间只能设置在 6 小时到 3 年之间。

当然,您可以为保护组添加多个调度任务,当前最多支持 10 个,按分钟、小时、天、周、月、年时间间隔进行自定义设置。

最后,检查确认创建保护组。

保护组创建成功后,默认不对保护组内的 VM 虚拟机创建快照,将根据设置的调度时间自动创建快照。

点击保护组,可以手动立即对保护组生成快照,或者重新编辑该保护组,暂停该保护组的调度任务,以及删除该保护组。

删除保护组有两种选项,第一种是“
保留快照直到过期日期”,当该保护组设置的调度任务中保留
快照的日期过期后删除保护组;第二个种是直接删除当前保护组生成的快照后然后删除保护组。

也可以点击进入该保护组,针对于当前特定保护组的显示信息以及所有相关操作。

手动点击“生成快照”,可以设置快照的名称以及快照保留的期限(至多久、多少天之前和永久),这不同于自动调度任务的设置。

点击“快照”,查看当前保护组所创建的所有快照,包含手动创建和自动任务创建的快照,可以选择某个快照执行“删除”操作。

点击“虚拟机”,查看当前保护组中所有虚拟机的快照状态,可以选择某个虚拟机执行“还原”和“克隆”操作。

二、虚拟机还原

vSAN 快照为非静默快照,vSAN 快照存储在
vSAN
数据存储本地。可以使用
vSAN
快照将虚拟机还原到快照保留的先前状态,例如因配置错误或更新失败而需要回滚的虚拟机。每个 VM 最多可以有 200 个快照,因此在制定计划等时需要考虑这一点。

现在测试在 VM1 虚拟机里创建一个 vm1.txt 文本文件,然后使用 vSAN Data Protection 为虚拟机生成一个 vSAN 快照,再到 VM1 虚拟机里将刚刚创建的 vm1.txt 文件删除,最后从 vSAN Data Protection 里还原该虚拟机,看 vm1.txt 文件是否被还原回来。

使用 vSAN Data Protection 为虚拟机手动生成一个 vSAN 快照。

然后到 VM1 虚拟机里删除 vm1.txt 文件。

现在还原该虚拟机,导航到 VM1 虚拟机-快照-vSAN Data Protection-快照管理,选择刚刚创建的快照点击“还原虚拟机”。

从 vSAN 快照还原虚拟机时,vSAN 会将当前虚拟机替换为快照虚拟机。虚拟机将关机并创建新快照作为恢复选项。

手动打开虚拟机的电源,登录到系统并查看文件,可以看到 vm1.txt 文件已经还原。

您还可以还原已在 vCenter 中已删除、迁移或取消注册但仍在集群中具有可用快照的虚拟机。这在因意外或者恶意删除虚拟机时,还原虚拟机变得十分有用。

现在,将 VM1 虚拟机关机,选择“从磁盘中删除”,虚拟机已从 vCenter 中完全删除。

这时,导航到 vSAN 集群-配置-vSAN-数据保护-虚拟机,点击“已移除的虚拟机”,可以看到 VM1 虚拟机的 vSAN 快照。

点击“还原虚拟机”,选项还原的快照名称,点击下一步。

设置还原虚拟机的名称及放置的文件夹,点击下一步。

设置还原虚拟机所使用的计算资源,点击下一步。

点击“还原”。

虚拟机从 vSAN 快照中还原成功,并继承之前虚拟机的 vSAN 快照。但是,如果要继续执行自动创建快照任务,需要将虚拟机重新添加至保护组。

三、虚拟机克隆

vSAN Data Protecation 支持增强的克隆功能,您可以从任意时间点创建的 vSAN 快照克隆一个新的虚拟机用于软件测试和开发等工作流。它利用高效的链接克隆,因为这些克隆有助于减少资源使用并加快开发周期。

例如,VM1 虚拟机在如下图中手动创建的 vSAN 快照名称是用于测试的 Version 1,现在需要基于这个版本克隆一个新的虚拟机用于其他测试。

点击“克隆虚拟机”,设置虚拟机的名称并选择放置的文件夹,点击下一步。

选择克隆虚拟机所使用的计算资源,点击下一步。

点击“克隆”。

克隆完成,现在你可以使用该虚拟机完成相关测试。

作为一个Java程序员,在日常的开发中,不必像C/C++程序员那样,为每一个内存的分配而操心,JVM会替我们进行自动的内存分配和回收,方便我们开发。但是一旦发生内存泄漏或者内存溢出,如果对Java内存结构不清楚,那将会是一件非常麻烦的事情!本文笔者将为大家详解Java内存结构。

面试tips

  1. 聊聊Java内存结构?都有哪些组成部分?哪些是线程共享?哪些是线程私有?
  2. 我们通常说的JVM调优,主要针对是哪一个区域?这个区域中那一块是最大的?主要用于存放什么内容?
  3. Java虚拟机栈存储的内容是什么?
  4. 程序计数器的作用是什么?当内存不足时,程序计数器会发生OOM吗?
  5. 聊聊你对方法区的看法?在不同JDK版本中,方法区的演进过程是什么?

你是否对这些问题都了如指掌?看完本文相信你心中就会有答案!

JVM架构

JVM的平台无关性

jvm与操作系统

  1. 计算机的CPU、内存、显卡等等属于
    硬件
  2. 常用的MacOs、Windows、Linux属于计算机的
    操作系统
  3. 而Java的虚拟机,也就是
    JVM是运行在操作系统
    之上的,与硬件没有直接联系,JVM也是Java能够跨平台的根本原因。

JVM架构

image-20240630174946980

1. Class Loader 类加载器

类加载器的作用是
加载类文件到内存
,比如编写一个 HelloWord.java 文件,然后通过 javac 编译成 class 文件,那怎么才能加载到内存中被执行呢?答案就是 Class Loader。当然,不是任何 .class 文件就能被加载的,Class Loader 加载的 class 文件是
有格式要求

2. Execution Engine 执行引擎

Class Loader 只负责加载,只要符合
文件
结构就加载,至于说能不能
运行
,则不是它负责的,那是由 Execution Engine 负责的。执行引擎也叫做解释器 (Interpreter),
负责解释命令,提交操作系统执行

3. Native Interface 本地接口

本地接口的作用是
融合不同的编程语言为 Java 所用
,它的初衷是融合 C/C++ 程序,Java 诞生的时候是 C/C++ 横行的时候,于是就在
内存中专门开辟了一块区域处理标记为 native 的代码

4. Runtime data area 运行时数据区

运行时数据区是
整个 JVM 的重点
。我们所有写的程序都被加载到这里,之后才开始运行,下面会重点讲解运行时数据区。

JVM执行流程

当然不同的VM的具体实现细节也不是不一样的,现在使用的比较多的JDK8版本就是
Sun HotSpot VM与BEA JRockit VM
合并之后开发出的JDK版本。

下面就是一个
Java文件加载并执行
的流程

JVM架构

运行时数据区

运行时数据区是JVM中最为重要的部分。也是我们在调优时需要重点关注的区域。

运行时数据区分为:
程序计数器

Java虚拟机栈

本地方法栈

Java堆区

方法区

其中

  • 线程私有
    :程序计数器、虚拟机栈、本地方法栈
  • 线程共享
    :堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、直接内存)

JDK 1.8 和之前的版本略有不同,我们这里以 JDK 1.7 和 JDK 1.8 这两个版本为例介绍。

JDK 1.7

java-runtime-data-areas-jdk1.7

JDK 1.8

java-runtime-data-areas-jdk1.8

程序计数器

程序计数寄存器(
Program Counter Register
),Register 的命名源于 CPU 的寄存器,寄存器存储指令相关的线程信息,CPU 只有把数据装载到寄存器才能够运行。它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域

  1. JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟
    。可以看作是当前线程所执行的字节码的
    行号指示器
    。解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

  2. 由于Java虚拟机的多线程是通过
    线程轮流切换
    、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,
    一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令
    。因此,为了线程切换后能恢复到正确的执行位置,
    每条线程都需要有一个独立的程序计数器

  3. 任何时间一个线程都只有一个方法在执行,也就是所谓的
    当前方法
    。如果当前线程正在执行的是 Java 方法,程序计数器记录的是 JVM 字节码指令地址,如果是执行 native 方法,则是未指定值(undefined)

  4. 程序计数器是唯一一个不会出现
    OutOfMemoryError
    的内存区域,它的生命周期与线程保持一致。

jvm-pc-counter

Java虚拟机栈

每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的
栈帧(Stack Frame)
,对应着一次次
Java 方法调用
,是线程私有的,生命周期和线程一致。

1、栈的内部结构

每个
栈帧
(Stack Frame)中存储着:

  • 局部变量表(Local Variables)
    :主要存放了编译期可知的
    各种数据类型
    (boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)

  • 操作数栈(Operand Stack)
    :主要用于存放方法执行过程中产生的
    中间计算结果
    。另外,计算过程中产生的临时变量也会放在操作数栈中。
    如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中

  • 动态链接(Dynamic Linking)
    :指向运行时常量池的方法引用。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用,当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。这个过程也被称为
    动态连接

  • 方法返回地址(Return Address)
    :方法正常退出或异常退出的地址

PS: 局部变量表中的变量也是重要的
垃圾回收根节点
,只要被局部变量表中直接或间接引用的对象都不会被回收

img

2、栈的执行流程

  • JVM 直接对虚拟机栈的操作只有两个:方法调用
    入栈
    ,方法执行结束
    出栈
  • 在线程中,同一时间只会有一个活动的栈帧,即(
    栈顶栈帧
    )是有效的,这个栈帧被称为
    当前栈帧
    (Current Frame),与当前栈帧对应的方法就是
    当前方法
    (Current Method),定义这个方法的类就是
    当前类
    (Current Class)
  • 如果在该方法中
    调用了其他方法
    ,对应的
    新的栈帧
    会被创建出来,放在栈的顶端,称为新的当前栈帧
  • 不同线程中所包含的栈帧是不允许相互引用的,即
    不可能在一个栈帧中引用另外一个线程的栈帧

jvm-stack-frame

3、栈的异常

  • StackOverFlowError

    若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出
    StackOverFlowError
    错误。

  • OutOfMemoryError

    如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出
    OutOfMemoryError
    异常。

本地方法栈

本地方法栈和Java虚拟机栈所发挥的作用非常相似

  • 二者区别在于:
    虚拟机栈为虚拟机执行 Java 方法
    (也就是字节码)服务,而
    本地方法栈则为虚拟机使用到的 Native 方法服务

  • 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

  • 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现
    StackOverFlowError

    OutOfMemoryError
    两种异常。


  • Hotspot JVM
    中,直接将本地方法栈和虚拟机栈合二为一

Java堆区

栈是运行时的单位,而堆是存储的单位

Java 堆是 Java 虚拟机管理的内存中最大的一块,被
所有线程共享

PS:
关于Java堆有很多细节可以深挖,例如堆的分代和对象的创建和回收等,后续我还会专门开一篇文章展开讲

1、堆的存储内容

此内存区域的唯一目的就是
存放对象实例
,几乎所有的对象实例以及数据都在这里分配内存。
成员变量名和值存储于堆中
,其生命周期和对象的是一致的。

Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

2、堆的分区和垃圾回收

为了进行高效的垃圾回收,虚拟机把堆内存
逻辑上
划分成三块区域(分代的唯一理由就是优化 GC 性能):

  • 新生带(年轻代)
    :新对象和没达到一定年龄的对象都在新生代
  • 老年代(养老区)
    :被长时间使用的对象,老年代的内存空间应该要比年轻代更大
  • 元空间(JDK1.8 之前叫永久代)
    :一些方法中的操作临时对象等,JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存

img

3、堆出现的异常

堆这里最容易出现的就是
OutOfMemoryError
错误,比如:

  • java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
    :当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。

  • java.lang.OutOfMemoryError: Java heap space
    :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过
    -Xmx
    参数配置,若没有特别配置,将会使用默认值,详见:
    Default Java 8 max heap sizeopen in new window
    )

方法区

方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域

1、方法区和永久代以及元空间是什么关系呢?

方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

method-area-implementation

2、方法区的存储内容

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的
类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
。在加载类和结构到虚拟机后,就会创建对应的
运行时常量池

运行时常量池(Runtime Constant Pool)
是虚拟机规范中是方法区的一部分,在加载类和结构到虚拟机后,就会创建对应的运行时常量池;而字符串常量池是这个过程中常量字符串的存放位置。所以从这个角度,字符串常量池属于虚拟机规范中的方法区,它是一个
逻辑上的概念
;而堆区,永久代以及元空间是实际的存放位置。

3、方法区在 JDK6、7、8中的演进细节

JDK版本 是否有永久代,字符串常量池放在哪里? 方法区逻辑上规范,由哪些实际的部分实现的?
jdk1.6及之前 有永久代,运行时常量池(包括字符串常量池),静态变量存放在永久代上 这个时期方法区在HotSpot中是由永久代来实现的,以至于
这个时期说方法区就是指永久代
jdk1.7 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中; 这个时期方法区在HotSpot中由
永久代
(类型信息、字段、方法、常量)和

(字符串常量池、静态变量)共同实现
jdk1.8及之后 取消永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中 这个时期方法区在HotSpot中由本地内存的
元空间
(类型信息、字段、方法、常量)和

(字符串常量池、静态变量)共同实现

method-area-jdk1.6

method-area-jdk1.7

参考文章

  1. 【002】十分钟搞懂Java内存结构

  2. JVM的内存分区/内存结构/内存区域/JVM内存模型

  3. JVM 基础 - JVM 内存结构

  4. Java内存区域详解(重点)

  5. 浅谈JVM整体架构与调优参数