-
1.前置知识1.1 Tomcat定义:Tomcat是一个开源的轻量级Web(Http)服务器和Servlet容器。它实现了Java Servlet等Java EE规范的核心功能,常用于部署和运行Java Web应用程序 。换言之,Tomcat就是一个严格遵循Servlet规范开发出来的、可以独立安装和运行的Java Web服务器/Servlet容器核心功能:Servlet容器:支持Servlet的执行,处理HTTP请求和响应Web服务器:提供静态资源(如HTML)的访问能力,支持基本的HTTP服务安装与版本对应:tomcat官网:Apache Tomcat®目录结构:bin:存放可执行文件,如startup.batconf:存放配置文件lib:存放Tomcat运行所需的jar文件logs:存储日志文件temp:存放临时文件,如上传的文件或缓存数据webapps:默认web应用部署目录work:服务器的工作目录,存放运行时生成的临时文件(编译文件)1.2 Servlet1.2.1 定义Servlet是Java语言编写的、运行在服务器端的程序,它遵循一套标准的API规范(Tomcat是这套规范的一个具体实现/容器,并提供了让Servlet与前端交互的运行时环境)1.2.2 API示范创建项目/配置文件:(1)在IEDA中创建Maven项目(2)在pom.xml文件中添加servlet依赖(置于< project >< /project >标签下)<dependencies> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <!--servlet依赖版本应与jdk和tomcat的版本相匹配--> <version>6.1.0</version> <scope>provided</scope> </dependency></dependencies>运行本项目xml(3)在main路径下创建webapp/Web-INF/web.xml,在xml文件中添加以下内容<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app> <display-name>Archetype Created Web Application</display-name></web-app>运行本项目xml(4)下载插件:Smart Tomcat(为了方便启动项目)API示例:import jakarta.servlet.annotation.WebServlet;import jakarta.servlet.http.HttpServlet;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;//设置访问路径(url)@WebServlet("/method")//继承HttpServlet并重写doGet和doPost方法public class MethodServlet extends HttpServlet { //接收method=post的请求 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doPost"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doPost"); } //接收method=put的请求 @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doPut"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doPut"); } //接收method=delete的请求 @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doDelete"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doDelete"); }}运行本项目java运行1.2.3 生命周期定义:Servlet 生命周期由 Web容器(如Tomcat)管理,包含加载、初始化、处理请求和销毁四个阶段。每个阶段对应特定的方法调用,开发者可通过重写这些方法实现自定义逻辑1.类加载:Web容器通过类加载器加载 Servlet 类(通常首次请求触发或容器启动时预加载,字节码文件被加载到内存但未实例化)。具体类加载流程请阅读:Java虚拟机——JVM(JavaVirtualMachine)解析一1.5 实例化:确认Servlet类成功加载后立刻执行,在整个Web容器中每种Servlet类(如HttpServlet)只会有一个实例化对象2.初始化:Web容器调用刚刚创建好的Servlet实例的init(ServletConfig config)方法,在整个servlet实例的生命周期中仅调用一次,主要作用是读取配置和资源加载。若初始化失败,抛出ServletException,Servlet不会被加入可用队列3.处理请求:Web容器为每个请求创建线程,调用service(ServletRequest req, ServletResponse res)方法。service() 方法根据HTTP请求类型(get/post)调用doGet()或doPost()4.销毁:Web容器调用 destroy()方法,Servlet 实例被标记为垃圾回收2.SpringBootServlet是Java EE规范中处理Web请求的核心组件,但随着应用复杂度提升,Servlet的直接使用显得笨重。Spring框架通过一系列抽象和扩展,简化了企业级应用开发我们可以用一个非常形象的比喻来贯穿始终:建造一座房子第一阶段:Servlet 时代 - 自己烧砖砌墙目标:建造一个能遮风挡雨的房子(一个能处理HTTP请求的Web应用)你的工作状态:材料: 你有最基础的原材料——泥土(Java 语言)和水(JVM)。你需要自己烧制砖块(编写Servlet类)工具: 只有简单的泥瓦刀(Servlet API)过程:1.你为每一面墙、每一扇门都亲手烧制一块特定的砖(编写 LoginServlet, UserServlet, OrderServlet)2.你亲自规划每块砖的位置(在web.xml中配置大量的 < servlet > 和 < servlet-mapping >)3.你亲自搅拌水泥,一块一块地砌墙(在每个 Servlet 的doGet/doPost 方法中手动解析参数、处理业务、组装 HTML)核心特点:高度灵活: 你可以造出任何形状的砖极其繁琐: 大量重复性劳动(每个 Servlet 都有获取参数、关闭连接等样板代码)难以维护: 砖块之间紧密耦合(对象依赖硬编码),想换一扇窗(修改功能)可能会牵动整面墙依赖外部: 房子建在别人的地上(需要将war包部署到外部的Tomcat服务器)总结:Servlet提供了Web开发的基础能力,但开发效率极低,代码冗余且难以维护第二阶段:Spring 时代 - 使用预制件和设计图纸目标:用更高效、更标准化的方式建造一个结构更好、更易扩展的房子你的工作状态:材料: 你不再烧砖,而是使用工厂提供的标准化预制件(Spring Bean,如 @Controller, @Service)核心创新: 你聘请了一位神奇的管家(IoC 容器)你不再亲自“砌砖”(用new实例化对象),只需告诉管家你需要什么(用@Autowired声明依赖)管家会自动把预制件(Bean)按照图纸(配置)组装好,送到你手上(依赖注入DI)过程:1.一个总大门(DispatcherServlet): 房子只有一个入口,所有访客(请求)都先到这里2.管家调度: 总大门处的接待员(DispatcherServlet)根据访客需求,呼叫房子里对应的专业房间(@Controller中的方法)来接待3.开发者只需专注于房间内的专业服务(业务逻辑),而不用关心访客是怎么进来的核心特点:解耦: 预制件之间是松耦合的,易于更换和测试专业化: AOP(面向切面编程)可以像“装修队”一样,非侵入式地为所有房间统一安装中央空调(日志、安全、事务)效率提升: 避免了大量重复劳动,结构清晰配置复杂: 绘制详细的“组装图纸”(配置 Spring)本身成了一项复杂的工作总结:Spring框架通过IoC/DI和AOP等理念,解决了代码耦合和重复劳动问题,但引入了显著的配置复杂度Spring Boot 1.0.0正式发布于2014年4月1日,标志着该框架的首次稳定版本发布。SpringBoot基于SpringFramework 4进行设计,显著减少了开发者的配置工作量,彻底消除了Spring的配置地狱约定大于配置:约定了默认配置Start机制:是一种依赖管理机制,每个Starter包含特定功能所需的依赖库和自动配置类,开发者只需引入对应Starter即可快速启用功能模块嵌入式容器:内置了Tomcat等嵌入式容器,无需部署war文件到外部容器,直接运行即可启动应用3.Spring Web MVC3.1 概述官方描述:Spring Web MVC是基于Servlet API构建的原始Web框架,并从一开始就在 Spring框架中。正式名称“Spring Web MVC”, 来自其源模块的名称(spring-webmvc),但它通常被称为“Spring MVC”MVC的起源与发展:MVC(Model-View-Controller)模式最初由挪威计算机科学家Trygve Reenskaug于1978年在施乐帕克研究中心(Xerox PARC)提出,目的是为Smalltalk编程语言设计用户界面。其核心思想是将应用程序的逻辑分为三个独立组件:Model:处理数据逻辑和业务规则View:负责数据展示和用户界面Controller:接收用户输入并协调Model与View的交互Spring MVC与MVC的关系:Spring MVC是MVC模式在Spring框架中的具体化,同时扩展了传统MVC的功能以适应现代Web开发需求3.2 必需工具Postman:主要用于 API 的开发和测试。它提供了一个用户友好的界面,支持发送HTTP请求、管理请求历史、自动化测试以及团队协作下载地址:Download PostmanFiddler:是一个网络调试代理工具,主要用于监控和分析HTTP/HTTPS流量。它可以捕获设备与服务器之间的所有请求和响应,支持修改请求、重放请求以及性能分析下载地址:Fiddler - Download3.3 RequestMapping作用:是Spring MVC中最核心、最基础的注解之一,用于将HTTP请求映射到具体的方法上注解级别:类+方法作为类注解:可以为整个类提供一个统一的url前缀(可有可无)作为方法注解:指定该方法负责处理哪个url的请求(强制要求)@RequestMapping("/HelloController")//@RestController声明该类是一个Spring MVC控制器@RestControllerpublic class HelloController { //0.不接收参数 //作为 方法注解 时,@RequestMapping(value = "/hello",method = RequestMethod.GET)等同于@GetMapping(value = "/hello") //设置返回的响应是json格式,produces = "application/json" @RequestMapping(value = "/hello",method = RequestMethod.GET,produces = "application/json") public String hello() { return "{\"Hello\" : World}"; } //1.一个参数 @RequestMapping("/receiveAge1") //不传参或者传递的参数名不匹配时默认为null public String receiveAge1(Integer age) { return "接收到参数 age:" + age; } @RequestMapping("/receiveAge2") //不传参或者传递的参数名不匹配时尝试设置为null,但int无法被设置为null,所以抛出IllegalStateException public String receiveAge2(int age) { return "接收到参数 age:" + age; } //2.接收数组 @RequestMapping("/receiveArray") public String receiveArray(String[] array) { return "接收到参数 array:" + Arrays.toString(array); } //3.接收对象,需要保证传递的参数名称和数量与Java对象保持一致 @RequestMapping("/receivePerson") public String receivePerson(Person person) { return "接收到参数 person:" + person; }}运行本项目java运行3.4 RequestBody作用:将HTTP请求体中的json数据绑定到Java对象(方法注解)注解级别:方法 @RequestMapping("/receivePerson") //@RequestBody接收JSON格式的数据 public String receivePerson(@RequestBody Person person) { return "接收到参数 person:" + person; }运行本项目java运行3.5 RequestParam作用:是Spring MVC框架中从HTTP请求中提取参数/查询字符串的注解,主要用于将请求参数绑定到控制器方法的参数上注解级别:方法 @RequestMapping("/receiveRename") //@RequestParam将url中key=name的查询字符串绑定到控制器的userName参数上 //required = false设置该参数为非必传(默认为true,必传) public String receiveRename(@RequestParam(value = "name",required = false) String userName) { return "接收到参数name:" + userName; }运行本项目java运行注意:需要接收多个同名参数时(如param=value1¶m=value2),直接绑定到List类型需通过该注解明确声明 @RequestMapping("/receiveList1") public String receiveList1(ArrayList<String> list) { //返回的list为空 return "接收到参数 list:" + list; }运行本项目java运行(1)在Spring MVC中,参数绑定机制对集合类型和数组类型的处理存在差异(2)使用ArrayList< String >作为方法参数时,必须显式添加@RequestParam注解,原因如下:默认绑定规则:Spring默认将单个请求参数的值绑定到简单类型(如 String、int)或单个对象。对于集合类型,框架无法自动推断是否需要将多个同名参数合并为集合需要明确指示:@RequestParam注解会告知Spring将同名请求参数的值收集到一个集合中(3)数组(如 String[])无需 @RequestParam 注解即可正确接收,原因如下:内置支持:Spring对数组类型有原生支持,能自动将多个同名请求参数值绑定到数组。这是框架的默认行为,无需额外配置 @RequestMapping("/receiveList2") public String receiveList2(@RequestParam(required = false) ArrayList<String> list) { //正确返回 return "接收到参数 list:" + list; }运行本项目java运行 @RequestMapping("/receiveList3") public String receiveList3(List<String> list) { //报错 return "接收到参数 list:" + list; }运行本项目java运行后端报错:java.lang.IllegalStateException: No primary or single unique constructor found for interface java.util.List。receiveList3方法使用List< String >接口类型而非具体实现类。Spring虽然支持接口类型参数绑定,但需要满足特定条件:必须配合@RequestParam注解使用不能直接使用未注解的接口类型参数报错根本原因:Spring尝试实例化List接口失败(接口不可实例化)3.6 PathVariable作用:用于从URL路径中提取变量值并绑定到方法的参数上注解级别:方法 @RequestMapping("/receivePath/{article}/{blog}") //required = false设置该参数为非必传(默认为true,必传) public String receivePath(@PathVariable(value = "article",required = false)Integer title,@PathVariable(value = "blog",required = false)String content) { return "接收到参数 article:" + title + " blog:" + content; }运行本项目java运行3.7 RequestPart作用:用于处理 HTTP 请求中的 multipart/form-data 类型数据,通常用于文件上传或同时上传文件和其他表单字段的场景注解级别:方法 @RequestMapping("/receiveFile") public String receiveFile(@RequestPart(value = "file",required = false) MultipartFile imgFile,@RequestParam(value = "userName",required = false) String name) { //返回原始的文件名 return "用户:" + name+ ",接收到文件:" +imgFile.getOriginalFilename(); }运行本项目java运行3.8 Controller&ResponseBody&RestControllerController作用:是Spring MVC中的核心注解,用于标记一个类作为Web请求的处理器(声明一个类是一个Spring MVC控制器),负责处理HTTP请求并返回视图注解级别:类ResponseBody作用:指示方法返回值应直接写入HTTP响应体,而非通过视图解析器渲染注解级别:类+方法@RequestMapping("/ControllerResponse")@Controllerpublic class ControllerResponse { //返回视图 @RequestMapping("/HTMLView") public String HTMLView(){ return "/show.html"; } //返回数据 @ResponseBody @RequestMapping("/HTMLData") public String HTMLData(){ return "/show.html"; }}运行本项目java运行RestController作用:是Spring MVC中的一个组合注解,它结合了@Controller和@ResponseBody的功能,标记的类所有方法返回值默认直接作为 HTTP 响应体(JSON/XML 等格式),无需额外视图渲染注解级别:类4.Gitee————————————————原文链接:https://blog.csdn.net/2401_89167985/article/details/148194312
-
【前言】在Java面向对象编程中,抽象类和接口是两个非常重要的概念,它们为代码的抽象化、模块化和可扩展性提供了强大的支持。无论是开发大型企业级应用,还是小型程序,掌握抽象类和接口的使用都至关重要。本文将通过详细的理论讲解、丰富的代码示例、直观的图片以及对比表格,帮助你深入理解Java抽象类和接口的本质与应用。文章目录:一、抽象类1.什么是抽象类2.抽象类语法3.抽象类特征3.1 抽象类不能实例化对象3.2 抽象方法不能是private的3.3 抽象方法不能被final和static修饰,因为抽象方法要被子类重写3.4 抽象类必须被继承,并且继承后子类要重写父类的抽象方法,除非子类也是抽象类,用abstract修饰3.5 抽象类中不⼀定包含抽象⽅法,但是有抽象⽅法的类⼀定是抽象类3.6 抽象类中可以有构造⽅法,供⼦类创建对象时,初始化⽗类的成员变量4.抽象类的作用二、接口1.什么是接口?2.语法规则3.接口的使用4.接口特性4.1 接口是一种引用类型,但是不能直接new接口的对象4.2 接口中的方法会被默认为public abstract,其他修饰符会报错4.3 接口中的方法不能在接口中实现,只能通过接口的类来实现4.4 重写接口方法时,不能使用默认的访问权限4.5 接口中可以有变量,但他会被默认为public static final 变量4.6 接口中不能有静态代码块和构造方法5.实现多个接口6.接口间的继承7.接口使用实例8.Clonable接口和深拷贝8.1 Clonable接口8.2 浅拷贝8.3 深拷贝9.抽象类和接口的区别一、抽象类1.什么是抽象类在面对对象的概念中,所以对象都是通过类来描述的,但并不是所有的类都是用来描述对象的,如果一个类中没有包含足够的信息来描述一个具体的对象,这样的类就是抽象类Dog类和Cat类都属于Animal类,但Dog()和Cat()方法没有实际的行为,可以将他们设计成抽象方法,包含抽象方法的类就是抽象类。2.抽象类语法在Java中,被abstract修饰的类称抽象类,抽象类中,被abstract修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。public abstract class Animal { abstract public void eat();//抽象方法:被abstract修饰,没有方法体 public double show(){ //自己增加的方法 return show1; } protected double show1;//参数}运行本项目java运行3.抽象类特征3.1 抽象类不能实例化对象public abstract class Animal { Animal animal = new Animal();}运行本项目java运行3.2 抽象方法不能是private的public abstract class Animal { abstract private void eat();//抽象方法:被abstract修饰,没有方法体}运行本项目java运行3.3 抽象方法不能被final和static修饰,因为抽象方法要被子类重写abstract final void eat();abstract public static void eat();运行本项目java运行3.4 抽象类必须被继承,并且继承后子类要重写父类的抽象方法,除非子类也是抽象类,用abstract修饰public abstract class Cat extends Animal { @Override public void eat(){ }}运行本项目java运行public abstract class Cat extends Animal { @Override public abstract void eat(); }运行本项目java运行3.5 抽象类中不⼀定包含抽象⽅法,但是有抽象⽅法的类⼀定是抽象类3.6 抽象类中可以有构造⽅法,供⼦类创建对象时,初始化⽗类的成员变量4.抽象类的作用抽象类本身不能被实例化,只能创建子类,并且重写抽象方法,这就起到检验的作用二、接口1.什么是接口?字面意思:我们生活中的水龙头,插座,充电线都是有接口的,才能插上。接口就是公共的行为规范标准,在实现时,只要符合规范标准,就能通用2.语法规则接口的定义和类基本相同,将class换成interface关键字public interface IShape { public static final int SIZE = 100;//接口变量默认public static final public abstract void test();//接口方法默认public abstract}运行本项目java运行【注意】接口命名一般以大写字母I开头;接口不能被实例化;3.接口的使用类与接口之间是implements的关系public class 类名 implenments 接口名{//...}运行本项目java运行这里建立一个IUSB接口和Mouse类public interface IUSB { void openDevice(); void closeDevice();}运行本项目java运行public class Mouse implements IUSB{ @Override public void openDevice() { System.out.println("打开鼠标"); } @Override public void closeDevice() { System.out.println("关闭鼠标"); }}运行本项目java运行4.接口特性4.1 接口是一种引用类型,但是不能直接new接口的对象public class TestUSB { public static void main(String[] args) { USB usb = new USB(); }}运行本项目java运行4.2 接口中的方法会被默认为public abstract,其他修饰符会报错public interface USB { private void openDevice();//private使用错误 void closeDevice();}运行本项目java运行4.3 接口中的方法不能在接口中实现,只能通过接口的类来实现public interface USB { //void openDevice(); void closeDevice(){ System.out.println("关闭USB设备"); }}运行本项目java运行4.4 重写接口方法时,不能使用默认的访问权限public class Mouse implements USB{ @Override public void openDevice() { System.out.println("打开鼠标"); }}运行本项目java运行4.5 接口中可以有变量,但他会被默认为public static final 变量public interface USB { int susu = 250;//默认被final public static修饰 void openDevice(); void closeDevice();}运行本项目java运行4.6 接口中不能有静态代码块和构造方法public interface USB { public USB(){ } { } int susu = 250;//默认被final public static修饰 void openDevice(); void closeDevice();}运行本项目java运行5.实现多个接口一个类可以实现多个接口,这就是继承所做不到的这里创建一个Dog类,可以让他实现多个接口,如IRunning,ISwimming,IFlying。每个接口的抽象方法都有重写,否则必须设置为抽象类AIT + InS快捷键重写public class Dog extends Animal implements IRunning,ISwimming{ @Override public void run() { } @Override public void swim() { }}运行本项目java运行狗:既能跑,又能游;【注意】有了接口之后,我们就不用注意具体类型,只需要关注这个类是否具备某种类型6.接口间的继承在Java中,类和类之间是单继承的,但一个类可以实现多个接口,接口与接口之间可以多继承这段代码就继承了两个接口:游和跑public class Dog extends Animal implements ISwimming,IRunning{ @Override public void run() { } @Override public void swim() { }}运行本项目java运行7.接口使用实例对象之间大小比较:public class Student { public String name; public int score; public Student (String name,int score){ this.name=name; this.score=score; } @Override public String toString() { return super.toString(); }}运行本项目java运行public class Test { public static void main(String[] args) { Student s1 = new Student("小华",20); Student s2 = new Student("小张",10); System.out.println(s1>s2); }}运行本项目java运行这样进行比较会报错,因为没有指定根据分数还是什么来比较,这样不太灵活,我们可以使用接口,如下:使用Comparable接口public class Student implements Comparable<Student>{ public String name; public int age; public Student (String name,int age){ this.name=name; this.age=age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) { if(this.age>o.age) return 1; else if(this.age == o.age) return 0; else return -1; } }运行本项目java运行public class Test { public static void main(String[] args) { Student s1 = new Student("小华",20); Student s2 = new Student("小张",12); //System.out.println(s1>s2); if(s1.compareTo(s2)>0){ System.out.println("s1>s2"); } }}运行本项目java运行使用Comparator接口import java.util.Comparator;public class AgeComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.age-o2.age; }}运行本项目java运行public class Test { public static void main(String[] args) { Student s1 = new Student("小华",20); Student s2 = new Student("小张",12); AgeComparator ageComparator=new AgeComparator(); int ret = ageComparator.compare(s1,s2); if(ret>0){ System.out.println("s1>s2"); }运行本项目java运行如果是根据名字来比较的,就要看对应字母的大小8.Clonable接口和深拷贝8.1 Clonable接口Java 中内置了⼀些很有⽤的接⼝,Clonable就是其中之⼀.Object 类中存在⼀个clone⽅法,调⽤这个⽅法可以创建⼀个对象的"拷⻉".但是要想合法调⽤clone⽅法,必须要先实现Clonable接⼝,否则就会抛出CloneNotSupportedException异常.public class Person implements Cloneable{ public String name; public int age; public Person (String name,int age){ this.name=name; this.age=age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}运行本项目java运行public class Test1 { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("小华子",20); Person person2 = (Person) person1.clone();//强制类型转换 System.out.println(person2); }}运行本项目java运行受查异常/编译时异常解决方法:8.2 浅拷贝class Money implements Cloneable{ public double money = 9.9;运行本项目java运行 @Override protected Object clone() throws CloneNotSupportedException { return super.clone();运行本项目java运行lic class Test1 { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("小华子",20); Person person2 = (Person) person1.clone(); System.out.println(person1.m.money); System.out.println(person2.m.money); System.out.println("=============="); person2.m.money = 19.9; System.out.println(person1.m.money); System.out.println(person2.m.money); }运行本项目java运行通过clone,我们只是拷⻉了Person对象。但是Person对象中的Money对象,并没有拷⻉。通过person2这个引⽤修改了m的值后,person1这个引⽤访问m的时候,值也发⽣了改变。这⾥就是发⽣了浅拷⻉。8.3 深拷贝class Money implements Cloneable{ public double money = 9.9; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}public class Person implements Cloneable{ public String name; public int age; public Money m = new Money(); public Person (String name,int age){ this.name=name; this.age=age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override protected Object clone() throws CloneNotSupportedException { //return super.clone(); Person tmp = (Person) super.clone(); tmp.m = (Money) this.m.clone(); return tmp; }}运行本项目java运行public class Test1 { public static void main(String[] args) throws CloneNotSupportedException{ Person person1 = new Person("小华子",20); Person person2 = (Person) person1.clone(); System.out.println(person1.m.money); System.out.println(person2.m.money); System.out.println("=============="); person2.m.money = 19.9; System.out.println(person1.m.money); System.out.println(person2.m.money); }运行本项目java运行核心代码:9.抽象类和接口的区别抽象类中可以包含普通⽅法和普通字段,这样的普通⽅法和字段可以被⼦类直接使⽤(不必重写),⽽接⼝中不能包含普通⽅法,⼦类必须重写所有的抽象⽅法.No 区别 抽象类(abstract) 接口(interface)1 结构组成 普通类+抽象方法 抽象方法+全局常量2 权限 各种权限 public3 子类使用 使用extends关键字继承抽象类 使用implements关键字实现接口4 关系 一个抽象类可以实现若干接口 接口不能继承抽象类,可extends继承多个父接口5 子类限制 一个子类只能继承一个抽象类 一个子类可以实现多个接口【总结】在 Java 里,抽象类与接口是实现抽象编程的关键工具。抽象类融合普通类与抽象方法,可定义部分实现逻辑,权限灵活;接口由抽象方法和全局常量构成,成员权限默认 public 。子类继承抽象类用 extends ,实现接口用 implements 。关系上,抽象类能实现多个接口,接口可多继承。继承限制方面,子类单继承抽象类,却可多实现接口 。二者各有适用场景,抽象类适合提炼共性、留存部分实现;接口利于规范行为、实现多态解耦。合理运用它们,能让代码架构更清晰、可扩展与可维护,助力构建灵活且健壮的 Java 程序 。————————————————原文链接:https://blog.csdn.net/user340/article/details/148381303
-
引言 在Java编程中,尤其是在使用匿名内部类时,许多开发者都会遇到这样一个限制:从匿名内部类中访问的外部变量必须声明为final或是"等效final"。这个看似简单的语法规则背后,其实蕴含着Java语言设计的深层考量。本文将深入探讨这一限制的原因、实现机制以及在实际开发中的应用。 一、什么是匿名内部类? 在深入讨论之前,我们先简单回顾一下匿名内部类的概念。匿名内部类是没有显式名称的内部类,通常用于创建只使用一次的类实例。 button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("Button clicked!"); }});一键获取完整项目代码java 二、final限制的历史与现状1、Java 8之前的严格final要求在Java 8之前,语言规范强制要求:任何被匿名内部类访问的外部方法参数或局部变量都必须明确声明为final// Java 7及之前版本public void process(String message) { final String finalMessage = message; // 必须声明为final new Thread(new Runnable() { @Override public void run() { System.out.println(finalMessage); // 访问外部变量 } }).start();}一键获取完整项目代码java 2、Java 8的等效final(effectively final)Java 8引入了一个重要改进:等效final的概念如果一个变量在初始化后没有被重新赋值,即使没有明确声明为final,编译器也会将其视为final,这就是"等效final"// Java 8及之后版本public void process(String message) { // message是等效final的,因为它没有被重新赋值 new Thread(new Runnable() { @Override public void run() { System.out.println(message); // 可以直接访问 } }).start(); // 如果取消下面的注释,会导致编译错误 // message = "modified"; // 这会使message不再是等效final的}一键获取完整项目代码java 三、为什么不能修改外部局部变量?1、变量生命周期不一致核心问题:方法参数和局部变量的生命周期与匿名内部类实例的生命周期不一致局部变量存在于栈帧上,其生命周期随着方法的结束而结束但是匿名内部类或 Lambda 表达式可能在方法返回后仍然存在(比如被传递给其他线程、存储在成员变量中等),如果它们直接使用方法的局部变量,而该变量已经被销毁,就会出问题解决方案:为了保证Lambda/内部类能访问到局部变量,Java并没有直接引用该变量,而是捕获了它的值的一个副本(拷贝)public void example() { int value = 10; // 局部变量,存在于栈帧中 Runnable r = new Runnable() { @Override public void run() { // 这里拿到的是value的副本,不是原始变量(引用地址不一样) System.out.println(value); } }; new Thread(r).start(); // 方法结束后,value的栈帧被销毁,value不复存在}一键获取完整项目代码java 2、数据一致性保证如果允许你修改一个外部局部变量,而Lambda使用的是值的拷贝,那么你修改了变量,但 Lambda 内部看不到这个修改(因为用的是拷贝)或者你误以为你修改了 Lambda 使用的那个值,但实际上你修改的是另一个东西允许修改会导致一种错觉:好像Lambda和外部共享了状态,其实不是// 假设Java允许这样做(实际上不允许)public void problematicExample() { int counter = 0; Runnable r = new Runnable() { @Override public void run() { // 假设允许访问,但 value 是拷贝的 0 System.out.println(counter); } }; counter = 5; // 修改原始变量 r.run(); // 输出0,你以为你改成了5}一键获取完整项目代码java 而且匿名内部类可能在另一个线程中执行,而原始变量可能在原始线程中被修改。final限制避免了线程安全问题3、解决方案如果确实需要“共享可变状态”,可以使用一个单元素数组、或者一个Atomicxxx类(如 AtomicInteger),或者将变量封装到一个对象中public class LambdaWorkaround { public static void main(String[] args) { int[] counter = {0}; // 使用数组来包装 Runnable r = () -> { counter[0]++; // 合法:修改的是数组内容,不是外部变量本身 System.out.println("Count: " + counter[0]); }; r.run(); // Count: 1 r.run(); // Count: 2 }}一键获取完整项目代码java 注意:这里你修改的是数组的内容,而不是变量 holder的引用,所以不违反规则 四、底层实现机制Java编译器通过以下方式实现这一特性: 值拷贝:编译器将final变量的值拷贝到匿名内部类中合成字段:在匿名内部类中创建一个合成字段来存储捕获的值构造函数传递:通过构造函数将捕获的值传递给匿名内部类实例可以通过反编译匿名内部类来观察这一机制: // 源代码public class Outer { public void method(int param) { Runnable r = new Runnable() { @Override public void run() { System.out.println(param); } }; }}一键获取完整项目代码java 反编译后的内部类和内部类大致如下:(参数自动添加final,内部类通过构造方法引入变量) // 反编译原始类 public class Outer { public void method(final int var1) { Runnable var10000 = new Runnable() { public void run() { System.out.println(var1); } }; }} // 反编译后能看到单独生成的匿名内部类class Outer$1 implements Runnable { Outer$1(Outer var1, int var2) { this.this$0 = var1; this.val$param = var2; } public void run() { System.out.println(this.val$param); }}一键获取完整项目代码java 五、常见问题与误区1、为什么实例变量没有这个限制?因为实例变量(成员变量)存储在堆(Heap)中,和对象生命周期一致而局部变量存储在栈(Stack)中,方法结束后就被销毁了Java 为保证 Lambda / 匿名内部类能安全访问变量,对这两者的处理方式完全不同public class Outer { private int instanceVar = 10; // 实例变量 public void method() { new Thread(new Runnable() { @Override public void run() { instanceVar++; // 可以直接修改实例变量 } }).start(); }}一键获取完整项目代码java 2、等效final的实际含义等效final意味着变量虽然没有明确声明为final,但符合final的条件:只赋值一次且不再修改public void effectivelyFinalExample() { int normalVar = 10; // 等效final final int explicitFinal = 20; // 明确声明为final // 两者都可以在匿名内部类中使用 Runnable r = () -> { System.out.println(normalVar + explicitFinal); }; // 如果这里修改变量,同样会编译报错 // normalVar = 5;}———————————————— 原文链接:https://blog.csdn.net/qq_35512802/article/details/151362542
-
前言 自古以来,中秋佳节便与圆月紧密相连,成为人们寄托思念与团圆之情的象征。在民间流传着这样一种说法:“十五的月亮十六圆”,仿佛这已成为一种铁律,深入人心。然而,这种说法是否真的站得住脚呢?在这背后,隐藏着怎样的天文奥秘?又是否可以通过科学的方法来验证这一传统观念呢?在科技飞速发展的今天,我们不妨借助编程的力量,运用Java语言来实证求解,揭开中秋满月的真相。 中秋赏月的传统由来已久,早在《周礼》中就有“中秋夜迎寒”的记载,而到了唐代,中秋赏月、玩月的风俗开始盛行。文人墨客们更是留下了许多描写中秋月夜的佳作,如苏轼的“但愿人长久,千里共婵娟”,将中秋的月与人间的思念紧密相连,赋予了中秋月深厚的文化内涵。在这样的文化背景下,“十五的月亮十六圆”这一说法也逐渐流传开来,成为人们茶余饭后的话题之一。然而,这种说法真的准确无误吗? 本文通过Java实证求解中秋满月的时间,不仅可以验证传统的说法,还可以更深入地了解天文学中的相关知识。这不仅是一次对传统观念的挑战,也是一次对科学方法的实践。无论最终的结果如何,这一过程都将让我们对中秋满月有更深刻的认识,也将让我们感受到科学的魅力和力量。在接下来的章节中,我们将详细介绍如何使用Java语言进行天文数据的处理和计算,以及如何通过模拟实验来验证“十五的月亮十六圆”这一说法。我们将逐步展开这一探索之旅,最终揭示中秋满月的真相。让我们一起踏上这段充满趣味和挑战的旅程,用科学的视角重新审视中秋的圆月,探索其中隐藏的奥秘。一、天文上的满月 在天文学中,月亮的圆缺变化是一个非常有趣且复杂的自然现象,这种变化主要源于月球绕地球的公转运动。月球绕地球运行一周的时间大约是29.5天,这个周期被称为一个“朔望月”。在这个周期中,月球相对于太阳的位置不断变化,从而导致我们从地球上看到的月相也随之改变。博主不是专业天文专业,这里仅分享一些简单的满月基础知识,让大家有一个概念。1、形成原理及定义 说到满月就必须提及月相,月相的形成是由于太阳光照射月球的不同部分,而我们从地球上看到的只是月球被太阳照亮的那一部分。随着月球绕地球的公转,被太阳照亮的部分逐渐增加,依次出现“娥眉月”“上弦月”“凸月”“满月”“下弦月”“残月”等不同的月相。其中满月是指月球完全被太阳照亮的那一面朝向地球,此时月球与太阳在地球的两侧,三者几乎在一条直线上。理论上,满月应该出现在农历的十五或十六,但实际的情况并非总是如此。由于月球的公转轨道是椭圆形的,且受到多种因素的影响,如地球的引力、太阳的引力等,月球的实际运行轨迹并非完全规律,因此满月出现的时间也会有所变化。2、出现时间及观测 “十五的月亮十六圆”这一说法广为流传,但实际上满月并不总是出现在农历的十六。根据天文观测数据,满月可能出现在农历的十四到十七之间的任何一天。例如,在某些年份,满月可能出现在农历十四的晚上,而在另一些年份,满月可能出现在农历十七的早晨。这种变化是由于月球的公转速度和轨道形状的不规则性所导致的。满月是观测月球的最佳时机之一,因为此时月球的整个盘面都被照亮,可以清晰地看到月球表面的山脉、陨石坑和月海等特征。在满月期间,月球的亮度会达到最大,这使得它在夜空中格外明亮。3、文化意义 在许多文化中,满月都具有重要的象征意义。在中国文化中,满月象征着团圆和完满,因此中秋节成为了家人团聚的重要节日。在西方文化中,满月也常常与神秘和浪漫联系在一起,许多文学作品和民间传说都以满月为背景。二、Java模拟月满计算 随着计算机技术的发展,我们有了更强大的工具来探索和验证这些天文现象。Java作为一种广泛使用的编程语言,具有强大的功能和灵活性,可以用来编写各种复杂的算法和程序。在本研究中,我们将利用Java语言编写程序,通过计算月球在不同时间的位置,来确定中秋满月的具体时间。我们将收集多年来的天文数据,包括月球的公转周期、轨道参数等,然后利用这些数据进行模拟计算。通过这种方式,我们可以得到一个较为准确的中秋满月时间表,从而验证“十五的月亮十六圆”这一说法的准确性。1、整体实现逻辑 使用Java求解中秋满月整体时间逻辑如下:public class MidAutumnFullMoonCalculator { // 主计算方法 public static Date calculateFullMoonTime(int year, int month, int day) { ... } // 核心天文算法 private static double calculateFullMoonJulianDay(double jd) { ... } // 辅助方法 private static double normalizeAngle(double angle) { ... } private static double calendarToJulianDay(Calendar cal) { ... } private static Calendar julianDayToCalendar(double jd) { ... }}一键获取完整项目代码java2、主计算方法详解 功能:这是程序的入口点,接收农历中秋的公历日期,返回精确的满月时刻,核心方法如下:/** * -计算指定农历中秋日期的月亮最圆时刻 * @param year 年份 * @param month 农历月份(八月) * @param day 农历日期(十五) * @return 月亮最圆时刻的Date对象 */public static Date calculateFullMoonTime(int year, int month, int day) { // 创建农历中秋日期(使用中午12点作为基准时间) Calendar midAutumnDate = Calendar.getInstance(); midAutumnDate.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); midAutumnDate.set(year, month - 1, day, 12, 0, 0); // month-1因为Calendar月份从0开始 // 计算精确的满月时刻 return calculatePreciseFullMoonTime(midAutumnDate);}一键获取完整项目代码java 参数说明:year:公历年份(如2024)month:公历月份(如9)day:公历日期(如17) 处理流程:创建Calendar对象,设置为北京时间将时间设为中午12点作为计算基准调用核心算法计算精确的满月时刻3、核心天文算法详解 核心算法是相关计算中最核心的内容,主要包括儒略日的计算、时间参数的计算、天文参数计算和周期项修正等内容,这里的天文计算采用近似计算,如需精度计算,请使用更精准的天文算法。3.1 儒略日计算基础/** * -计算满月时刻的儒略日 * -基于Jean Meeus的天文算法 */private static double calculateFullMoonJulianDay(double jd) { // 计算从2000年1月6日(基准新月)开始的月相周期数 double k = Math.floor((jd - 2451550.09765) / 29.530588853); // 满月对应的k值(新月+0.5) k = k + 0.5; // 计算T(儒略世纪数) double T = k / 1236.85; //----其它计算}一键获取完整项目代码java 儒略日(Julian Day):天文学中常用的连续时间计数法,从公元前4713年1月1日格林尼治平午开始计算。月相周期数k:2451550.09765:2000年1月6日18:14的儒略日,作为一个基准新月时刻29.530588853:一个朔望月的平均长度(天)k:从基准时间开始经过的月相周期数k + 0.5:从新月到满月是半个周期3.2 时间参数计算// 计算T(儒略世纪数)double T = k / 1236.85; // 计算基础儒略日double JDE = 2451550.09765 + 29.530588853 * k + 0.0001337 * T * T - 0.000000150 * T * T * T + 0.00000000073 * T * T * T * T;一键获取完整项目代码java T(儒略世纪数):以36525天为一世纪的时间单位,用于高阶项的计算。 (儒略历书日):考虑了长期项修正的基础满月时刻。3.3 天文参数计算 // 计算太阳平近点角 double M = normalizeAngle(2.5534 + 29.10535669 * k - 0.0000218 * T * T - 0.00000011 * T * T * T); // 计算月亮平近点角double Mprime = normalizeAngle(201.5643 + 385.81693528 * k + 0.1017438 * T * T + 0.00001239 * T * T * T - 0.000000058 * T * T * T * T); // 计算月亮升交点平黄经double F = normalizeAngle(160.7108 + 390.67050274 * k - 0.0016341 * T * T - 0.00000227 * T * T * T + 0.000000011 * T * T * T * T); // 计算Omega(月亮轨道升交点经度)double Omega = normalizeAngle(124.7746 - 1.56375580 * k + 0.0020691 * T * T + 0.00000215 * T * T * T);一键获取完整项目代码java 天文参数说明:M(太阳平近点角):太阳在轨道上的平均位置角度系数:2.5534° + 29.10535669°/周期反映地球公转轨道的椭圆性影响M'(月亮平近点角):月亮在轨道上的平均位置角度系数:201.5643° + 385.81693528°/周期反映月球公转轨道的椭圆性影响F(月亮升交点平黄经):月球轨道与黄道交点的平均位置系数:160.7108° + 390.67050274°/周期反映月球轨道平面的进动Ω(月亮轨道升交点经度):更精确的轨道交点位置系数:124.7746° - 1.56375580°/周期3.4 周期项修正计算// 转换为弧度double M_rad = Math.toRadians(M);double Mprime_rad = Math.toRadians(Mprime);double F_rad = Math.toRadians(F);double Omega_rad = Math.toRadians(Omega);// 计算周期项修正double correction = 0;// 主要修正项correction += -0.40720 * Math.sin(Mprime_rad);correction += 0.17241 * 0.016708617 * Math.sin(M_rad);correction += 0.01608 * Math.sin(2 * Mprime_rad);correction += 0.01039 * Math.sin(2 * F_rad);correction += 0.00739 * 0.016708617 * Math.sin(Mprime_rad - M_rad);correction += -0.00514 * 0.016708617 * Math.sin(Mprime_rad + M_rad);correction += 0.00208 * 0.016708617 * 0.016708617 * Math.sin(2 * M_rad);correction += -0.00111 * Math.sin(Mprime_rad - 2 * F_rad);correction += -0.00057 * Math.sin(Mprime_rad + 2 * F_rad);correction += 0.00056 * 0.016708617 * Math.sin(2 * Mprime_rad + M_rad);correction += -0.00042 * Math.sin(3 * Mprime_rad);correction += 0.00042 * 0.016708617 * Math.sin(M_rad + 2 * F_rad);correction += 0.00038 * 0.016708617 * Math.sin(M_rad - 2 * F_rad);correction += -0.00024 * 0.016708617 * Math.sin(2 * Mprime_rad - M_rad);correction += -0.00017 * Math.sin(Omega_rad);correction += -0.00007 * Math.sin(Mprime_rad + 2 * M_rad);correction += 0.00004 * Math.sin(2 * Mprime_rad - 2 * F_rad);correction += 0.00004 * Math.sin(3 * M_rad);correction += 0.00003 * Math.sin(Mprime_rad + M_rad - 2 * F_rad);correction += 0.00003 * Math.sin(2 * Mprime_rad + 2 * F_rad);correction += -0.00003 * Math.sin(Mprime_rad + M_rad + 2 * F_rad);correction += 0.00003 * Math.sin(Mprime_rad - M_rad + 2 * F_rad);correction += -0.00002 * Math.sin(Mprime_rad - M_rad - 2 * F_rad);correction += -0.00002 * Math.sin(3 * Mprime_rad + M_rad);correction += 0.00002 * Math.sin(4 * Mprime_rad); // 应用修正double preciseJDE = JDE + correction;一键获取完整项目代码java修正项原理:每个修正项都对应一个特定的天文效应:-0.40720 × sin(M'):月球椭圆轨道的主要修正(中心差)0.17241 × e × sin(M):地球轨道偏心率对月相的影响0.01608 × sin(2M'):月球轨道的二阶椭圆项0.01039 × sin(2F):月球轨道倾角的影响0.00739 × e × sin(M' - M):地球和月球轨道相互影响-0.00514 × e × sin(M' + M):地球和月球轨道的组合效应e = 0.016708617:地球轨道偏心率这些修正项基于布朗月球运动理论,考虑了月球轨道的各种摄动因素。4、辅助方法详解 本小节将对辅助方法进行简单介绍。4.1 角度标准化/** * -将角度标准化到0-360度范围内 */private static double normalizeAngle(double angle) { angle = angle % 360; if (angle < 0) { angle += 360; } return angle;}一键获取完整项目代码java 功能:将角度限制在0-360度范围内,避免数值溢出。4.2 日历与儒略日转换/** * -将Calendar转换为儒略日 */private static double calendarToJulianDay(Calendar cal) { int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH) + 1; int day = cal.get(Calendar.DAY_OF_MONTH); int hour = cal.get(Calendar.HOUR_OF_DAY); int minute = cal.get(Calendar.MINUTE); int second = cal.get(Calendar.SECOND); double decimalHour = hour + minute / 60.0 + second / 3600.0; if (month <= 2) { year--; month += 12; } int a = year / 100; int b = 2 - a + a / 4; return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + decimalHour / 24.0 + b - 1524.5;}一键获取完整项目代码java 转换公式:标准的天文儒略日计算公式,考虑了:闰年规则格里高利历改革(1582年)时间的小数部分处理4.3 儒略日转日历/** * -将儒略日转换为Calendar */private static Calendar julianDayToCalendar(double jd) { jd += 0.5; double z = Math.floor(jd); double f = jd - z; double a; if (z < 2299161) { a = z; } else { double alpha = Math.floor((z - 1867216.25) / 36524.25); a = z + 1 + alpha - Math.floor(alpha / 4); } double b = a + 1524; double c = Math.floor((b - 122.1) / 365.25); double d = Math.floor(365.25 * c); double e = Math.floor((b - d) / 30.6001); double day = b - d - Math.floor(30.6001 * e) + f; int month = (int) (e < 14 ? e - 1 : e - 13); int year = (int) (month > 2 ? c - 4716 : c - 4715); double time = day - Math.floor(day); int hour = (int) (time * 24); int minute = (int) ((time * 24 - hour) * 60); int second = (int) Math.round((((time * 24 - hour) * 60 - minute) * 60)); // 处理秒数进位 if (second >= 60) { second = 0; minute++; } if (minute >= 60) { minute = 0; hour++; } Calendar cal = Calendar.getInstance(); cal.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); cal.set(year, month - 1, (int) Math.floor(day), hour, minute, second); cal.set(Calendar.MILLISECOND, 0); return cal;}一键获取完整项目代码java 关键点:jd += 0.5:儒略日从中午开始,调整为从午夜开始处理格里高利历改革(1582年10月4日后跳过10天)精确的时间分量计算三、近年中秋满月计算及对比 本节将结合实例对每年的中秋月满时间进行计算,通过本小节就可以获取每年的满月日期和具体的时间,并且与官方提供的时间进行对比,大家通过对比就可以知晓问题的开始,是不是所有的月亮都是十六圆了。 1、近年中秋满月计算/** * -测试方法 - 计算未来几年的中秋节月亮最圆时刻 */public static void main(String[] args) { // 已知的农历中秋日期(公历日期) int[][] midAutumnDates = { {2019, 9, 13}, // 2019年中秋节 {2020, 10, 1}, // 2020年中秋节 {2021, 9, 21}, // 2021年中秋节 {2022, 9, 10}, // 2022年中秋节 {2023, 9, 29}, // 2023年中秋节 {2024, 9, 17}, // 2024年中秋节 {2025, 10, 6}, // 2025年中秋节 {2026, 9, 25}, // 2026年中秋节 }; SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒"); sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); System.out.println("中秋节月亮最圆时刻计算结果:"); System.out.println("================================="); for (int[] date : midAutumnDates) { int year = date[0]; int month = date[1]; int day = date[2]; Date fullMoonTime = calculateFullMoonTime(year, month, day); System.out.printf("%d年中秋节(公历%d月%d日)月亮最圆时刻: %s%n", year, month, day, sdf.format(fullMoonTime)); }}一键获取完整项目代码java 接下来我们在IDE中运行意以上成就可以得到以下结果: 以上就是实现一个从2019年到2026年,跨度为7年的中秋满月计算过程。2、近年计算与公布时间对比 通过以上7年的计算,再结合官方公布的满月日期及时刻,来对比一下我们的计算方法与官方公布的时间相差是多少?年份 中秋(公历) 满月时间(本地) 是否当天 满月时间(官方公布) 误差2019 2019-9-13 09月14日 08时39分21秒 否(十六) 9月14日12时33分 3时54分2020 2020-10-1 10月02日 01时28分17秒 否(十六) 10月2日 05时5分 3时37分2021 2021-9-21 09月21日 03时58分38秒 是(十五) 9月21日 07时54分 3时56分2022 2022-9-10 09月10日 13时34分12秒 是(十五) 9月10日 17时59分 4时25分2023 2023-9-29 09月29日 13时49分05秒 是(十五) 9月29日 17时58分 4时8分2024 2024-9-17 09月18日 06时15分39秒 否(十六) 9月18日 10时34分 4时19分2025 2024-10-06 10月07日 07时41分10秒 否(十六) 10月7日 11时48分 4时7分 结合近七年的满月日期及时刻来看,并不是所有的中秋月圆都是十六圆,有的是当天就圆了。所以,从这个角度来定义,十五的月亮十六圆可不是准确的哦。通过这种本地近似的计算,虽然在具体的时刻上有一些误差,但是日期是与官方公布的是完全一致的,时刻的误差通过近7年的验证,相差时间在4个小时左右,所以未来可以结合更长序列的时间进行相应的修正。四、总结 以上就是本文的主要内容,本文通过Java实证求解中秋满月的时间,不仅可以验证传统的说法,还可以更深入地了解天文学中的相关知识。这不仅是一次对传统观念的挑战,也是一次对科学方法的实践。无论最终的结果如何,这一过程都将让我们对中秋满月有更深刻的认识,也将让我们感受到科学的魅力和力量。通过Java满月近似求解,并结合2019年到2025年的中秋满月日期时刻的计算,得出了重要的一个结论,十五的月亮不一定十六圆,通过严谨的程序计算得到的数据支撑。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。————————————————原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/152717752
-
一、AQS 是什么?AQS,全称 AbstractQueuedSynchronizer,即抽象队列同步器。抽象:它是一个抽象类,本身不能直接实例化,需要子类去继承它,并实现其保护方法来管理同步状态。队列:它内部维护了一个先进先出(FIFO)的等待队列,用于存放那些没有抢到锁的线程。同步器:它是构建锁和其他同步组件(如 Semaphore、CountDownLatch 等)的基础框架。核心思想:AQS 使用一个整型的 volatile 变量(state) 来表示同步状态(例如,锁被重入的次数、许可的数量等),并通过一个内置的 FIFO 队列来完成资源获取线程的排队工作。设计模式:AQS 是 模板方法模式 的经典应用。父类(AQS)定义了骨架和核心算法,而将一些关键的操作以 protected 方法的形式留给子类去实现。这样,实现一个自定义同步器只需要关注如何管理 state 状态即可,至于线程的排队、等待、唤醒等复杂操作,AQS 已经帮我们完成了。二、AQS 的核心结构AQS 的核心可以概括为三部分:同步状态(state)、等待队列 和 条件队列。1. 同步状态(State)这是一个 volatile int 类型的变量,是 AQS 的灵魂。private volatile int state;一键获取完整项目代码它的具体含义由子类决定,非常灵活:在 ReentrantLock 中,state 表示锁被同一个线程重复获取的次数。state=0 表示锁空闲,state=1 表示锁被占用,state>1 表示锁被重入。在 Semaphore 中,state 表示当前可用的许可数量。在 CountDownLatch 中,state 表示计数器当前的值。对 state 的操作是原子的,通过 getState(), setState(int newState), compareAndSetState(int expect, int update) 等方法进行。2. 等待队列(CLH 队列的变体)这是一个双向链表,是 AQS 实现阻塞锁的关键。当线程请求共享资源失败时,AQS 会将当前线程以及等待状态等信息构造成一个节点(Node) 并将其加入队列的尾部,同时阻塞该线程。头节点(Head):指向获取到资源的线程所在的节点。头节点不持有线程,是一个“虚节点”。尾节点(Tail):指向队列中最后一个节点。当一个线程释放资源时,它会唤醒后继节点,后继节点成功获取资源后,会将自己设置为新的头节点。主要原理图如下:AQS 使用一个 Volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。3. 条件队列(Condition Object)AQS 内部类 ConditionObject 实现了 Condition 接口,用于支持 await/signal 模式的线程间协作。每个 ConditionObject 对象都维护了一个自己的单向链表(条件队列)。当线程调用 Condition.await() 时,会释放锁,并将当前线程构造成节点加入条件队列,然后阻塞。当线程调用 Condition.signal() 时,会将条件队列中的第一个等待节点转移到 AQS 的等待队列中,等待重新获取锁。注意:一个 AQS 实例可以对应多个 Condition 对象(即多个条件队列),但只有一个等待队列。三、AQS 的设计与关键方法AQS 将资源获取的方式分为两种:独占模式(Exclusive):一次只有一个线程能执行,如 ReentrantLock。共享模式(Shared):多个线程可以同时执行,如 Semaphore、CountDownLatch。AQS 提供了顶层的入队和出队逻辑,而将尝试获取资源和尝试释放资源的具体策略留给了子类。需要子类重写的关键方法(Protected)这些方法在 AQS 中是 protected 的,默认抛出 UnsupportedOperationException。独占模式:boolean tryAcquire(int arg):尝试以独占方式获取资源。成功返回 true,失败返回 false。boolean tryRelease(int arg):尝试以独占方式释放资源。成功返回 true,失败返回 false。共享模式:int tryAcquireShared(int arg):尝试以共享方式获取资源。负数表示失败;0 表示成功,但后续共享获取可能失败;正数表示成功,且后续共享获取可能成功。boolean tryReleaseShared(int arg):尝试以共享方式释放资源。其他:boolean isHeldExclusively():当前同步器是否在独占模式下被线程占用。在 Condition 相关操作中会用到。供外部调用的重要方法(Public)这些是模板方法,子类一般不重写,使用者(或子类)直接调用。独占模式:void acquire(int arg):以独占模式获取资源,忽略中断。如果获取失败,会进入等待队列。void acquireInterruptibly(int arg):同上,但响应中断。boolean tryAcquireNanos(int arg, long nanosTimeout):在 acquireInterruptibly 基础上增加了超时限制。boolean release(int arg):以独占模式释放资源。共享模式:void acquireShared(int arg):以共享模式获取资源。void acquireSharedInterruptibly(int arg):响应中断的共享获取。boolean tryAcquireSharedNanos(int arg, long nanosTimeout):带超时的共享获取。boolean releaseShared(int arg):以共享模式释放资源。四、源码级工作流程解析(以 acquire 为例)我们来看一下最核心的 acquire 方法,它展示了 AQS 的完整工作流程:public final void acquire(int arg) { if (!tryAcquire(arg) && // 1. 尝试直接获取资源(子类实现) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 获取失败,则加入队列;3. 在队列中自旋/阻塞等待 selfInterrupt(); // 如果在等待过程中被中断,补上中断标记}一键获取完整项目代码tryAcquire(arg):这是子类实现的方法。比如在 ReentrantLock 的非公平锁实现中,它会直接尝试使用 CAS 修改 state,如果成功,就将当前线程设置为独占线程。如果 tryAcquire 成功,整个 acquire 方法就结束了,线程继续执行。如果失败,进入下一步。addWaiter(Node.EXCLUSIVE):创建一个代表当前线程的 Node 节点,模式为独占模式(Node.EXCLUSIVE)。通过 CAS 操作,快速地将这个新节点设置为尾节点。如果失败,则进入 enq(node) 方法,通过自旋 CAS 的方式确保节点被成功添加到队列尾部。acquireQueued(final Node node, int arg):这是核心中的核心。节点入队后,会在这个方法里进行自旋(循环)等待。在循环中,它会检查自己的前驱节点是不是头节点(p == head)。如果是,说明自己是队列中第一个等待的线程,会再次调用 tryAcquire 尝试获取资源(因为此时锁可能刚好被释放了,这是一个避免不必要的线程挂起、提高性能的优化)。如果获取成功,就将自己设为新的头节点,然后返回。如果前驱不是头节点,或者再次尝试获取失败,则会调用 shouldParkAfterFailedAcquire 方法,检查并更新前驱节点的状态(比如将其 waitStatus 设置为 SIGNAL,表示“当你释放锁时,需要唤醒我”)。如果一切就绪,就调用 parkAndCheckInterrupt() 方法,使用 LockSupport.park(this) 阻塞(挂起)当前线程。当线程被唤醒后(通常是由前驱节点释放锁时 unpark 的),会再次检查自己是否是头节点的后继,并重复上述自旋过程,直到成功获取资源。selfInterrupt():如果在等待过程中线程被中断,acquireQueued 方法会返回 true,这里会调用 selfInterrupt 补上中断标志,因为 AQS 在 acquire 过程中是忽略中断的。释放流程(release)相对简单:public final boolean release(int arg) { if (tryRelease(arg)) { // 1. 子类尝试释放资源 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 2. 唤醒后继节点 return true; } return false;}一键获取完整项目代码unparkSuccessor 会找到队列中第一个需要唤醒的线程(通常是头节点的下一个有效节点),然后调用 LockSupport.unpark(s.thread) 将其唤醒。五、AQS 的应用举例AQS 是 JUC 包的基石,几乎所有的同步工具都基于它:ReentrantLock:使用 AQS 的独占模式,state 表示重入次数。ReentrantReadWriteLock:读写锁。AQS 的 state 高16位表示读锁状态,低16位表示写锁状态。Semaphore:使用 AQS 的共享模式,state 表示可用许可数。CountDownLatch:使用 AQS 的共享模式,state 表示计数器值。countDown() 是 releaseShared,await() 是 acquireShared。ThreadPoolExecutor:其内部的工作线程 Worker 类,也继承了 AQS,用于实现独占锁,来判断线程是否空闲。六、总结AQS 的核心贡献在于,它提供了一个强大的框架,将复杂的线程排队、阻塞、唤醒等底层操作封装起来,让同步器的开发者只需要关注一个核心问题:如何管理那个 state 变量。它的优点:极大地降低了构建锁和同步器的复杂度。性能高效:通过自旋、CAS 等无锁编程技术,减少了线程上下文切换的开销。灵活强大:通过两种模式的区分,可以构建出各种复杂的同步工具。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/153201201
-
Spring AI 1.0 GA 深度解析:Java生态的AI革命已来作者按:在经历了8个里程碑版本的迭代后,Spring AI 1.0 GA于2025年5月20日正式发布。作为Spring生态的官方AI框架,它标志着Java开发者正式迈入AI原生应用时代。本文基于生产环境实践,深度剖析其核心架构与落地策略。一、为什么Spring AI是Java开发者的AI"入场券"?1.1 从Spring Boot到AI Boot:技术演进的必然想象一下这个场景:你的团队需要在现有Spring微服务中集成AI能力,但面对OpenAI、通义千问、Claude等不同API时,每个都需要单独适配。更痛苦的是,当业务需要从聊天机器人升级到RAG知识库,再到多Agent协作时,代码重构的噩梦就开始了。这正是Spring AI要解决的问题。它就像当年Spring整合JDBC、Hibernate一样,现在统一了AI领域的"混沌"。基于我们的生产实践,使用Spring AI后:代码量减少60%,模型切换成本从2人周降至2小时。 1.2 企业级AI的三座大山传统AI集成面临的核心痛点: 模型碎片化:OpenAI、Claude、通义千问各自为政 技术栈割裂:Python AI与Java业务系统难以融合 生产就绪度低:缺乏监控、限流、熔断等企业级能力 运行本项目Spring AI通过三层抽象完美解决:ChatClient:统一所有大模型调用VectorStore:屏蔽向量数据库差异Advisor:AOP式增强AI能力二、核心架构:比LangChain更懂Java的设计哲学2.1 ChatClient:AI世界的JDBCTemplate@RestControllerpublicclassSmartController{privatefinalChatClient chatClient;publicSmartController(ChatClient.Builder builder){this.chatClient = builder .defaultSystem("你是一个专业的Java架构师").build();}@GetMapping("/ai/code-review")publicCodeReviewreviewCode(@RequestParamString code){return chatClient.prompt().user("请分析这段代码的设计模式:{code}", code).call().entity(CodeReview.class);// 直接返回结构化对象}}运行本项目性能数据:在我们的压测中,ChatClient相比原生HTTP调用:平均延迟降低35%,内存使用减少40%2.2 向量数据库的"USB-C"接口Spring AI支持20种向量数据库的统一抽象,性能对比实测:数据库 百万级向量QPS 延迟P99 最佳场景Milvus 1200 15ms 大规模图像检索Weaviate 800 25ms 知识图谱场景Chroma 200 80ms 原型开发PGVector 500 40ms 已有PostgreSQL配置示例:spring:ai:vectorstore:milvus:host: localhost port:19530index-type: HNSW metric-type: COSINE 运行本项目2.3 Advisor:AI领域的Spring AOP通过拦截器链实现模型增强,核心Advisor对比:Advisor类型 作用 性能开销QuestionAnswerAdvisor RAG检索增强 +15msChatMemoryAdvisor 会话记忆 +5msSafeGuardAdvisor 敏感词过滤 +2ms三、生产级RAG架构实战3.1 亿级文档的RAG流水线基于某金融客户的真实案例,架构演进过程:阶段1:简单RAG(100万文档)@BeanpublicRetrievalAugmentationAdvisorragAdvisor(){returnRetrievalAugmentationAdvisor.builder().documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(milvusVectorStore).similarityThreshold(0.75).topK(5).build()).queryAugmenter(ContextualQueryAugmenter.builder().maxTokens(500).build()).build();}运行本项目阶段2:分布式RAG(1000万文档)引入MultiQueryExpansion:提升召回率30%DocumentReRanker:使用Cross-Encoder重排序混合检索:向量+BM25混合打分阶段3:实时RAG(亿级文档)增量索引:Kafka实时同步文档变更缓存策略:Redis缓存热点查询负载均衡:多向量库分片存储3.2 性能调优秘籍向量维度优化:实测发现:1536维vs768维在准确率上仅差2%,但存储减少50%建议:业务场景优先768维,精度敏感再用1536维分块策略:// 智能分块:按语义完整性切割DocumentSplitter splitter =newTokenTextSplitter(800,// 每块最大token200,// 重叠token5,// 最小块数10000// 最大token);运行本项目四、Function Calling:让AI"动手"的魔法4.1 从天气预报到股票交易工具定义:@ServicepublicclassStockService{@Tool(description ="获取股票实时价格")publicStockPricegetPrice(String symbol){return webClient.get().uri("/stock/{symbol}", symbol).retrieve().bodyToMono(StockPrice.class).block();}@Tool(description ="执行股票交易")publicTradeResultexecuteTrade(@ToolParam(description ="股票代码")String symbol,@ToolParam(description ="交易数量")int quantity,@ToolParam(description ="交易类型")TradeType type){// 实际交易逻辑}}运行本项目实测数据:工具调用成功率:99.2%(基于10000次调用)平均响应时间:180ms(含API往返)错误恢复:自动重试3次,指数退避4.2 复杂业务流程编排Agent工作流模式:模式类型 适用场景 代码复杂度Chain 顺序任务流 Parallel 批量处理 Routing 智能分流 Orchestrator 动态任务分解 实战案例:订单处理AgentpublicclassOrderAgent{publicvoidprocessOrder(String orderRequest){OrchestratorWorkersWorkflow workflow =newOrchestratorWorkersWorkflow(chatClient);// 1. 分析订单 -> 2. 检查库存 -> 3. 计算价格 -> 4. 生成发货单OrderResult result = workflow.process(orderRequest);}}运行本项目五、MCP协议:AI生态的TCP/IP5.1 什么是MCP?模型上下文协议(Model Context Protocol)就像AI世界的HTTP协议,它让任何AI应用都能:发现可用工具标准化调用方式安全权限控制Spring AI MCP架构:┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ AI App │────│ MCP Client │────│ MCP Server │ │ (ChatGPT) │ │ (Spring AI) │ │ (Weather) │ └─────────────┘ └──────────────┘ └─────────────┘ 运行本项目5.2 企业级MCP实践安全控制:@ConfigurationpublicclassMcpSecurityConfig{@BeanpublicSecurityFilterChainmcpSecurity(HttpSecurity http)throwsException{ http .requestMatchers("/mcp/**").authenticated().oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);return http.build();}}运行本项目性能监控:MCP调用监控:通过Micrometer导出QPS、延迟指标工具健康检查:集成Spring Boot Actuator审计日志:记录每次工具调用的参数与结果六、性能基准测试:真实数据说话6.1 测试环境硬件:16核CPU, 64GB内存, SSD模型:gpt-4-turbo, qwen-plus并发:1000虚拟用户6.2 关键指标对比场景 Spring AI 原生HTTP 提升简单问答 120ms 180ms 33%RAG查询 350ms 520ms 33%工具调用 200ms 280ms 29%内存使用 800MB 1.2GB 33%6.3 生产调优参数spring:ai:chat:options:temperature:0.7max-tokens:1000retry:max-attempts:3backoff:multiplier:2max-delay: 5s circuitbreaker:failure-rate-threshold: 50% wait-duration-in-open-state: 30s 运行本项目七、企业落地路线图7.1 三阶段演进策略阶段1:试点验证(2-4周)选择非核心业务场景(如内部知识问答)使用Chroma+OpenAI快速原型建立监控和评估体系阶段2:核心场景(2-3个月)迁移到Milvus企业级向量库集成Spring Cloud微服务体系实现多模型路由策略阶段3:全面AI化(6-12个月)构建企业AI能力中台实现MCP生态集成建立AI治理体系7.2 避坑指南技术陷阱: 直接在生产环境使用Chroma(超过100万文档性能急剧下降) 忽视Token成本控制(实测GPT-4每月账单可达数万美元)缺少限流熔断(大促期间API额度耗尽导致服务雪崩)最佳实践: 使用分层架构:原型用Chroma,生产用Milvus实现Token预算管理:按用户/业务线配额部署本地模型兜底:Ollama+Llama2作为备用八、未来展望:Java AI原生时代Spring AI 1.0 GA的发布不是终点,而是Java AI生态的起点。随着以下特性的roadmap逐步实现:2025 Q3:多模态支持(图像、音频处理)2025 Q4:分布式Agent框架2026 Q1:AI工作流可视化编排2026 Q2:自动模型优化与压缩我们可以预见,未来3年内:80%的Java企业应用将具备AI能力,而Spring AI将成为这个时代的"Spring Boot"。 附录:快速开始指南1. 创建项目spring init --dependencies=web,ai my-ai-app 运行本项目2. 配置模型spring:ai:openai:api-key: ${OPENAI_API_KEY}dashscope:api-key: ${DASHSCOPE_API_KEY}运行本项目3. 第一个AI接口@SpringBootApplicationpublicclassAiApplication{publicstaticvoidmain(String[] args){SpringApplication.run(AiApplication.clas从0到1只需30分钟,这就是Spring AI的魅力。————————————————原文链接:https://blog.csdn.net/mr_yuanshen/article/details/153403301
-
前言首先,我们必须明确一个核心观点:在分布式环境下,要实现强一致性(在任何时刻读取的数据都是最新的)是极其困难且代价高昂的,通常会严重牺牲性能。因此,在实践中,我们通常追求最终一致性,即允许在短暂的时间内数据不一致,但通过一些手段保证数据最终会保持一致。下面我将从基础概念、各种策略、最佳实践到最新方案,为你详细讲解。一、基础概念:为什么会有不一致?在一个包含 MySQL(作为可靠数据源)和 Redis(作为缓存)的系统中,所有的写操作(增、删、改)都必须同时处理这两个地方。 这个过程中,任何一步失败或延迟都会导致不一致:写 MySQL 成功,写 Redis 失败:导致 Redis 中是旧数据。写 Redis 成功,写 MySQL 失败:导致 Redis 中是“脏数据”,数据库中不存在。并发读写:一个线程在更新数据库,但还没更新缓存时,另一个线程读取了旧的缓存数据。二、核心策略与模式解决双写一致性有多种策略,我们需要根据业务场景(对一致性的要求、读写的比例等)进行选择。策略一:Cache-Aside Pattern(旁路缓存模式)这是最常用、最经典的缓存模式。核心原则是:应用程序直接与数据库和缓存交互,缓存不作为写入的必经之路。读流程:收到读请求。首先查询 Redis,如果数据存在(缓存命中),直接返回。如果 Redis 中没有数据(缓存未命中),则从 MySQL 中查询。将从 MySQL 查询到的数据写入 Redis(以便后续读取),然后返回数据。写流程:收到写请求。更新 MySQL 中的数据。删除 Redis 中对应的缓存。为什么是删除(Invalidate)缓存,而不是更新缓存?这是一个关键设计点!性能:如果更新缓存,每次数据库写操作都要伴随一次缓存写操作,如果该数据并不经常被读取,那么这次缓存写入就是浪费资源的。并发安全:在并发写场景下,更新缓存的顺序可能与更新数据库的顺序不一致,导致缓存中是旧数据。而删除操作是幂等的,更为安全。Cache-Aside 如何保证一致性?它通过“先更新数据库,再删除缓存”来尽力保证。但它依然存在不一致的窗口期:线程 A 更新数据库。线程 B 读取数据,发现缓存不存在,从数据库读取旧数据(因为 A 还没提交或刚提交)。线程 B 将旧数据写入缓存。线程 A 删除缓存。这种情况发生的概率较低,因为通常数据库写操作(步骤1)会比读操作(步骤2)耗时更长(因为涉及锁、日志等),所以步骤2在步骤1之前完成的概率很小。但这是一种理论上的可能。策略二:Write-Through / Read-Through Pattern(穿透读写模式)在这种模式下,缓存层(或一个独立的服务)自己负责与数据库交互。对应用来说,它只与缓存交互。写流程:应用写入缓存,缓存组件同步地写入数据库。只有两个都成功后才会返回成功。读流程:应用读取缓存,如果未命中,缓存组件自己从数据库加载并填充缓存,然后返回。优点:逻辑对应用透明,一致性比 Cache-Aside 更好。缺点:性能较差,因为每次写操作都必然涉及一次数据库写入。通常需要成熟的缓存中间件支持。策略三:Write-Behind Pattern(异步写回模式)Write-Through 的异步版本。应用写入缓存后立即返回,缓存组件在之后某个时间点(例如攒够一批数据或定时)批量异步地更新到数据库。优点:写性能极高。缺点:有数据丢失风险(缓存宕机),一致性最弱。适用于允许少量数据丢失的场景,如计数、点赞等。三、保证最终一致性的进阶方案为了弥补 Cache-Aside 模式中的缺陷,我们可以引入一些额外的机制。方案一:延迟双删针对 Cache-Aside 中提到的“先更新数据库,再删除缓存”可能带来的并发问题,可以引入一个延迟删除。线程 A 更新数据库。线程 A 删除缓存。线程 A 休眠一个特定的时间(如 500ms - 1s)。线程 A 再次删除缓存。第二次删除是为了清理掉在第1次删除后、其他线程可能写入的旧数据。这个休眠时间需要根据业务读写耗时来估算。优点:简单有效,能很大程度上解决并发读写导致的不一致。缺点:降低了写入吞吐量,休眠时间难以精确设定。方案二:通过消息队列异步删除为了解耦和重试,可以将删除缓存的操作作为消息发送到消息队列(如 RocketMQ, Kafka)。更新数据库。向消息队列发送一条删除缓存的消息。消费者消费该消息,执行删除 Redis 的操作。如果删除失败,消息会重试。这保证了删除缓存的操作至少会被执行一次,大大提高了可靠性。方案三:通过数据库 Binlog 同步(最优解)这是目前最成熟、对业务侵入性最小、一致性最好的方案。其核心是利用 MySQL 的二进制日志(Binlog)进行增量数据同步。工作原理:业务系统正常写入 MySQL。由一个中间件(如 Canal, Debezium)伪装成 MySQL 的从库,订阅 Binlog。中间件解析 Binlog,获取数据的变更详情(增、删、改)。中间件根据变更,调用 Redis 的 API 来更新或删除对应的缓存。优点:业务无侵入:业务代码只关心写数据库,完全不知道缓存的存在。高性能:数据库和缓存的同步是异步的,不影响主业务链路的性能。强保证:由于基于 Binlog,它能保证只要数据库变了,缓存最终一定会被同步。顺序也与数据库一致。缺点:架构复杂,需要维护额外的同步组件。同步有毫秒级到秒级的延迟。四、总结与最佳实践选择策略一致性保证性能复杂度适用场景Cache-Aside + 删除最终一致性(有微弱不一致风险)高低绝大多数场景的首选,读多写少Cache-Aside + 延迟双删更好的最终一致性中低对一致性要求稍高,且能接受一定延迟的写操作Write-Through强一致性中中写多读少,且对一致性要求非常高的场景Binlog 同步最终一致性(推荐)高高大型、高要求项目的最佳实践,对业务无侵入通用建议:首选方案:对于大多数应用,从 Cache-Aside(先更新数据库,再删除缓存) 开始。它简单、有效,在大多数情况下已经足够。进阶保障:如果 Cache-Aside 的不一致窗口无法接受,可以引入延迟双删或消息队列异步删除来增强。终极方案:当业务发展到一定规模,对一致性和系统解耦有更高要求时,投入资源搭建基于 Binlog 的异步同步方案。这是业界证明最可靠的方案。设置合理的过期时间:无论如何,都给 Redis 中的缓存设置一个过期时间(TTL)。这是一个安全网,即使同步逻辑出现问题,旧数据也会自动失效,最终从数据库加载新数据,保证最终一致性。业务容忍度:最重要的是,与产品经理确认业务对一致性的容忍度。很多时候,1-2秒内的数据不一致用户是感知不到的,不需要为此付出巨大的架构和性能代价。————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/153478251
-
如何使用update-alternatives管理多版本Java JDK?(Windows、Mac、Ubuntu)摘要在实际开发中,往往会遇到既要维护老项目又要跟进新特性的场景,这就需要在一台机器上同时安装并切换多个Java JDK版本。本文将针对三大主流平台——Windows、macOS 和 Ubuntu,详细介绍如何安装多个 JDK,并使用各自平台上的“替代方案”工具来管理与切换。Windows:通过系统环境变量与批处理脚本实现版本切换macOS:利用 /usr/libexec/java_home 与 jEnv 工具Ubuntu:深入剖析 update-alternatives 原理与实战无论您是新手还是有一定经验的开发者,都能从中获得清晰的思路与操作指南。文章目录如何使用update-alternatives管理多版本Java JDK?(Windows、Mac、Ubuntu) 摘要引言作者名片 加入我们AI共创团队 加入猫头虎的共创圈,一起探索编程世界的无限可能! 正文1. Windows 平台1.1 环境变量基础1.2 安装多个 JDK1.3 手动切换1.4 使用批处理脚本自动切换2. macOS 平台2.1 `/usr/libexec/java_home` 命令2.2 使用 jEnv 统一管理(推荐)3. Ubuntu 平台(Debian系)3.1 `update-alternatives` 原理3.2 安装与注册 JDK3.2.1 使用 APT 安装(OpenJDK)3.2.2 手动下载并注册 Oracle JDK3.3 切换与查看查看当前注册项交互式切换4. 验证与示例5. 常见问题与解决6. 常见 QA总结粉丝福利联系我与版权声明 引言多版本 JDK 切换为何如此重要?兼容性测试:老项目可能依赖 Java 8,而新项目需要 Java 17。生态差异:Spring Boot 2.x 与 3.x 对 Java 版本的要求不同。CI/CD 集成:自动化构建需要在不同 JDK 下验证构建过程。三大平台各有生态与管理方式,因此本文将分别展开,帮助您在不同系统上搭建灵活的多版本 Java 环境。作者名片 博主:猫头虎全网搜索关键词:猫头虎作者微信号:Libin9iOak作者公众号:猫头虎技术团队更新日期:2025年07月21日欢迎来到猫头虎的博客 — 探索技术的无限可能!加入我们AI共创团队 猫头虎AI共创社群矩阵列表:点我进入共创社群矩阵入口点我进入新矩阵备用链接入口加入猫头虎的共创圈,一起探索编程世界的无限可能! 正文1. 🪟 Windows 平台1.1 环境变量基础Windows 管理可执行程序的核心是 系统路径(PATH) 与 环境变量(Environment Variables)。切换 JDK 版本,本质上就是让系统在 PATH 中优先找到对应版本的 java.exe 与 javac.exe。1.2 安装多个 JDK从 Oracle 官网或 AdoptOpenJDK 下载所需版本的 Windows 安装包(.exe)。依次安装到不同目录,如:C:\Program Files\Java\jdk1.8.0_381C:\Program Files\Java\jdk-17.0.71.3 手动切换打开系统环境变量:右键「此电脑」→「属性」→「高级系统设置」→「环境变量」。找到 系统变量 中的 JAVA_HOME、Path:修改 JAVA_HOME 为目标 JDK 目录。在 Path 里,将 %JAVA_HOME%\bin 放到最前面。点击「确定」,重新打开命令行窗口,即可 java -version 验证。1.4 使用批处理脚本自动切换为了避免每次手动修改环境变量,可编写简单的 .bat 脚本:@echo offREM 切换到 Java 8setx JAVA_HOME "C:\Program Files\Java\jdk1.8.0_381" /Msetx PATH "%%JAVA_HOME%%\bin;%%PATH%%" /Mecho 已切换到 Java 8一键获取完整项目代码bat保存为 switch-to-java8.bat,右键以管理员身份运行。同理可写 switch-to-java17.bat。运行后重启命令行窗口即可生效。2. macOS 平台2.1 /usr/libexec/java_home 命令macOS 自带命令 /usr/libexec/java_home,可列出并切换已安装的 JDK 版本。# 列出所有已安装JDK/usr/libexec/java_home -V# 切换到 Java 11export JAVA_HOME=$(/usr/libexec/java_home -v 11)export PATH=$JAVA_HOME/bin:$PATH一键获取完整项目代码bash-V:显示版本列表及安装路径。-v <version>:选择指定版本。将上述两行写入 ~/.zshrc 或 ~/.bash_profile,并配合 alias:alias j8='export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)'alias j11='export JAVA_HOME=$(/usr/libexec/java_home -v 11)'alias j17='export JAVA_HOME=$(/usr/libexec/java_home -v 17)'一键获取完整项目代码bash打开新终端后,输入 j11 即可切换。2.2 使用 jEnv 统一管理(推荐)jEnv 是跨平台的 Java 版本管理工具,支持 macOS、Linux。安装 jEnv(需先安装 Homebrew):brew install jenv一键获取完整项目代码bash将 jEnv 集成到 shell 配置:echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrcecho 'eval "$(jenv init -)"' >> ~/.zshrcsource ~/.zshrc一键获取完整项目代码bash添加已安装的 JDK:jenv add /Library/Java/JavaVirtualMachines/jdk1.8.0_381.jdk/Contents/Homejenv add /Library/Java/JavaVirtualMachines/jdk-17.0.7.jdk/Contents/Home一键获取完整项目代码bash列出与切换:jenv versionsjenv global 11 # 全局切换到 Java 11jenv local 1.8 . # 针对当前目录切换到 Java 1.8jenv shell 17 # 仅对当前 shell 有效一键获取完整项目代码bashjEnv 会自动管理 JAVA_HOME 与 PATH,并支持插件扩展(Maven、Gradle 插件等)。3. Ubuntu 平台(Debian系)3.1 update-alternatives 原理Debian/Ubuntu 引入 alternatives 系统,允许对系统命令(如 java、javac)创建“组”,并在组内注册多个“备选项”。每个备选项由 可执行文件路径 和 优先级 组成。运行 update-alternatives --config <name> 即可交互式切换。3.2 安装与注册 JDK3.2.1 使用 APT 安装(OpenJDK)sudo apt updatesudo apt install -y openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk一键获取完整项目代码bashAPT 安装后通常会自动注册到 alternatives,您可以直接执行下一步。3.2.2 手动下载并注册 Oracle JDK下载并解压到 /usr/lib/jvm:sudo mkdir -p /usr/lib/jvmsudo tar -xzf ~/Downloads/jdk-17.0.7_linux-x64_bin.tar.gz -C /usr/lib/jvm一键获取完整项目代码bash注册到 alternatives(以 Java 17 为例,优先级设为 2):sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk-17.0.7/bin/java 2sudo update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/jdk-17.0.7/bin/javac 2sudo update-alternatives --install /usr/bin/jar jar /usr/lib/jvm/jdk-17.0.7/bin/jar 2一键获取完整项目代码bash第三个参数为命令组名(可省略后缀)。最后一个数字为优先级,数值越大越优先。3.3 切换与查看查看当前注册项update-alternatives --query java一键获取完整项目代码bash输出包含所有 java 备选路径及当前选择。交互式切换sudo update-alternatives --config java一键获取完整项目代码bash会列出所有已注册的 Java 可执行文件,按提示输入对应序号即可切换。同理切换 javac、jar 等。4. 验证与示例无论在哪个平台,切换后都应首先验证:java -versionjavac -version一键获取完整项目代码bash并可编写最简单的 HelloWorld 程序进行编译与运行测试。5. 常见问题与解决场景 原因与排查 解决思路切换后 java -version 仍指向旧版本 PATH 未更新或 shell 缓存未刷新 重新打开终端;Windows 重启 CMD;Linux hash -rWindows 脚本执行报 “权限不足” 未以管理员身份运行 .bat 右键 → “以管理员身份运行”macOS /usr/libexec/java_home 列不全 JDK 未正确安装到 /Library/Java/... 检查 JDK 文件夹;重启 shellUbuntu 手动注册后未见新选项 alternatives 配置不一致 再次执行 --install;检查路径拼写6. 常见 QAQ:为什么 Linux 上要用 update-alternatives?A:它能同时管理多个版本的同名命令,避免手动修改 PATH,且支持优先级与脚本化。Q:Windows 有没有类似 update-alternatives 的工具?A:官方没有,但可借助 jabba 或自定义批处理脚本。Q:macOS 上除了 jEnv 还有其他方案吗?A:也可使用 SDKMAN! 管理,但 SDKMAN! 对 Windows 支持有限。总结本文深入对比了 Windows、macOS 和 Ubuntu 三大平台上多版本 Java JDK 管理的思路与实践:Windows:环境变量 + 批处理脚本macOS:/usr/libexec/java_home + jEnvUbuntu:update-alternatives 原理详解掌握上述方法后,无论在本地开发还是在 CI/CD 环境,都能灵活切换 JDK 版本,确保兼容性与高效协同开发。祝您 Java 开发之路顺畅————————————————原文链接:https://blog.csdn.net/qq_44866828/article/details/149533211
-
本文简介目的:Spring生态为Java后端开发提供了强大支持,但将分散的技术点整合成完整解决方案往往令人困惑。本文将以登录接口为切入点,系统演示如何将IOC/DI、MyBatis数据持久化、MD5加密、Session/Cookie管理、JWT令牌和拦截器机制融合运用,打造企业级认证方案技术栈:前端:HTML + CSS + JavaScript + Jquery后端:SpringBoot + Mybatis + JWT搭建环境:数据库:MySQL8.4.0项目结构:maven前端框架:Jquery后端框架:SpringBootJDK:17编译器:IDEA目录结构:项目搭建及配置1.创建SpringBoot3.0.0+项目并添加依赖:Spring Web、MyBatis Framework、MySQL Driver、Lombok2.初始化数据库:create database spring_blog_login charset utf8mb4; use spring_blog_login;create table user_info (id int primary key auto_increment,user_name varchar(128) unique , password varchar(128) not null,delete_flag int default 0, create_time datetime default now(),update_time datetime default now());insert into user_info (user_name,password) values ('张三','123456'), ('李四','123456'), ('王五','123456');运行本项目sql3.将application.properties修改为application.yml并添加如下配置:spring: datasource: url: jdbc:mysql://127.0.0.1:3306/spring_blog_login?characterEncoding=utf8&useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Drivermybatis: configuration: map-underscore-to-camel-case: true #自动驼峰转换server: port: 8080 #不显式设置默认为8080运行本项目yml按住Ctrl + F5,如果程序能运行成功则说明搭建及配置都没问题(MySQL服务器必须要处于运行状态)1.登录认证全栈实现 ->基础版1.1 后端实现1.1.1 架构设计本次登录功能采用Controller、Service、Mapper三层架构:Controller层依赖于Service层来执行业务逻辑并获取处理结果,而Service层又依赖于Mapper层来进行数据持久化操作1.1.2 实体类实体类用于封装业务数据,需要与数据库表结构一一对应import lombok.Data;import java.util.Date;@Datapublic class UserInfo { private Integer id; private String userName; private String password; private Integer deleteFlag; private Date createTime; private Date updateTime;}运行本项目java运行1.1.3 Controller处理HTTP请求、参数校验、返回响应import org.example.springlogin.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/user")public class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @RequestMapping("/login") public String login(String userName,String password) { if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) { return "用户或密码为空"; } return userService.getUserInfoByUserName(userName,password); }}运行本项目java运行1.1.4 Service业务逻辑处理import org.example.springlogin.mapper.UserMapper;import org.example.springlogin.model.UserInfo;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class UserService { private final UserMapper userMapper; @Autowired public UserService(UserMapper userMapper) { this.userMapper = userMapper; } public String getUserInfoByUserName(String userName,String password) { UserInfo userInfo = userMapper.getUserInfoByUserName(userName); if (userInfo == null) { return "用户不存在"; } if (!password.equals(userInfo.getPassword())) { return "密码错误"; } return "登录成功"; }}运行本项目java运行1.1.5 Mapper数据持久化操作import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Select;import org.example.springlogin.model.UserInfo;@Mapperpublic interface UserMapper { @Select("select * from user_info where user_name = #{userName}") UserInfo getUserInfoByUserName(String userName);}运行本项目java运行1.2 前端实现Gitee:项目前端代码,Gitee上的前端代码是最新提交的,如下效果图仅作参考效果演示:1.用户或密码为空2.用户不存在3.密码错误4.登录成功2.Cookie/SessionHTTP(超文本传输协议)设计为无状态协议,指服务器默认不保留客户端请求之间的任何状态信息。每个请求独立处理,服务器不会记忆之前的交互内容(如下图)优点:请求独立性:每次请求被视为新请求,服务器不依赖历史请求数据简单高效:无状态设计降低服务器资源消耗,简化实现逻辑缺点:身份识别困难:需通过额外机制(如Cookies、Session)跟踪用户状态重复传输数据:每次请求需携带完整信息,可能增加冗余(如认证信息)cookie:是存储在客户端(浏览器)的小型文本数据,由服务器通过HTTP响应头Set-Cookie发送给客户端,并在后续请求中自动携带session:是存储在服务器端的用户状态信息,通常通过一个唯一的Session ID标识,该ID可能通过Cookie或URL传递如上图片引用自我的博客:Java EE(13)——网络原理——应用层HTTP协议,服务器内部实际上专门开辟了一个session空间用于存储用户信息,每当新用户发送第一次请求时服务器会将用户信息存储在session中并生成一个session id通过Set-Cookie方法返回给客户端,即cookiesession结构如下:修改Controller类代码:import jakarta.servlet.http.HttpSession;import lombok.extern.slf4j.Slf4j;import org.example.springlogin.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;@RestController@RequestMapping("/user")@Slf4jpublic class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @RequestMapping("/login") public String login(String userName, String password, HttpSession session) { log.info("接收到参数,userName:{},password:{}",userName,password); if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) { return "用户或密码为空"; } String result = userService.getUserInfoByUserName(userName, password); if (result.equals("登录成功")){ HashMap<String,String> map = new HashMap<>(); map.put("userName",userName); map.put("password",password); //将map作为用户信息存储到session/会话中 session.setAttribute("cookie", map); log.info("登录成功"); } return result; }运行本项目java运行修改前端代码: function login() { $.ajax({ url: '/user/login', type: "post", data:{ userName:$('#username').val(), password:$('#password').val(), }, success: function(result) { alert(result); }, }) }运行本项目javascript运行Fiddler抓包结果:前端/浏览器按住Ctrl + Shift + i打开控制台点击应用程序/application,打开Cookie:3.统一返回结果封装统一返回结果封装是后端开发中的重要设计模式,能够保持API响应格式的一致性,便于前端处理1.创建枚举类:统一管理接口或方法的返回状态码和描述信息,标准化业务逻辑中的成功或失败状态import lombok.Getter;@Getterpublic enum ResultStatus { SUCCESS(200,"成功"), FAIL(-1,"失败"), ; private final Integer code; private final String message; ResultStatus(Integer code, String message) { this.code = code; this.message = message; }}运行本项目java运行2.创建Result< T >类:主要用于规范服务端返回给客户端的响应数据格式。通过固定结构(状态码、错误信息、数据)确保前后端交互的一致性import lombok.Data;@Data//通过泛型<T>设计,可以灵活封装任意类型的数据对象到data字段public class Result<T> { //业务码 private ResultStatus code; //错误信息 private String errorMessage; //数据 private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(ResultStatus.SUCCESS); result.setErrorMessage(null); result.setData(data); return result; } public static <T> Result<T> fail(String errorMessage) { Result<T> result = new Result<>(); result.setCode(ResultStatus.FAIL); result.setErrorMessage(errorMessage); result.setData(null); return result; }}运行本项目java运行3.修改Controller代码:@RestController@RequestMapping("/user")@Slf4jpublic class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @RequestMapping("/login") public Result<String> login(String userName, String password, HttpSession session) { log.info("接收到参数,userName:{},password:{}",userName,password); if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) { return Result.fail("用户或密码为空"); } String result = userService.getUserInfoByUserName(userName, password); if (!result.equals("登录成功")){ return Result.fail(result); } HashMap<String,String> map = new HashMap<>(); map.put("userName",userName); map.put("password",password); //将map作为用户信息存储到session/会话中 session.setAttribute("cookie", map); log.info("登录成功"); return Result.success(result); }}运行本项目java运行4.修改前端代码: function login() { $.ajax({ url: '/user/login', type: "post", data:{ userName:$('#username').val(), password:$('#password').val(), }, success: function(result) { if (result.code === "SUCCESS") { alert(result.data) }else { alert(result.error) } }, }) }运行本项目javascript运行4.图形验证码图形验证码(captcha)是一种区分用户是人类还是自动化程序的技术,主要通过视觉或交互任务实现。其核心意义体现在以下方面:防止自动化攻击:通过复杂图形或扭曲文字,阻止爬虫、暴力破解工具等自动化程序批量注册或登录,降低服务器压力提升安全性:在敏感操作(如支付、修改密码)前增加验证步骤,减少数据泄露或恶意操作风险Hutool提供了CaptchaUtil类用于快速生成验证码,支持图形验证码和GIF动态验证码。在pom.xml文件中添加图下配置:<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <!-- 版本号应与springboot版本兼容 --> <version>5.8.40</version></dependency>运行本项目xml1.创建CaptchaController类,用于生成验证码并返回给前端import cn.hutool.captcha.CaptchaUtil;import cn.hutool.captcha.LineCaptcha;import jakarta.servlet.http.HttpServletResponse;import jakarta.servlet.http.HttpSession;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.io.IOException;@RestController@RequestMapping("/captcha")@Slf4jpublic class CaptchaController { //设置过期时间 public final static long delay = 60_000L; @RequestMapping("/get") public void getCaptcha(HttpSession session, HttpServletResponse response) { log.info("getCaptcha"); LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100); //设置返回类型 response.setContentType("image/jpeg"); //禁止缓存 response.setHeader("Pragma", "No-cache"); try { //通过响应输出生成的图形验证码 lineCaptcha.write(response.getOutputStream()); //保存code session.setAttribute("CAPTCHA_SESSION_CODE", lineCaptcha.getCode()); //保存当前时间 session.setAttribute("CAPTCHA_SESSION_DATE", System.currentTimeMillis()); //关闭输出流 response.getOutputStream().close(); } catch (IOException e) { throw new RuntimeException(e); } }}运行本项目java运行2.修改前端代码:最终版<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>微信登录</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="css/login.css"></head><body> <div class="login-container"> <div class="logo"> <i class="fab fa-weixin"></i> </div> <h2>微信登录</h2> <form id="loginForm"> <div class="input-group"> <i class="fas fa-user"></i> <label for="username"></label><input type="text" id="username" placeholder="请输入用户名" required> </div> <div class="input-group"> <i class="fas fa-lock"></i> <label for="password"></label><input type="password" id="password" placeholder="请输入密码" required> </div> <div class="input-group"> <div class="captcha-container"> <label for="inputCaptcha"></label><input type="text" id="inputCaptcha" class="captcha-input" placeholder="输入验证码"> <img id="verificationCodeImg" src="/captcha/get" class="captcha-img" title="看不清?换一张" alt="验证码"> </div> </div> <div class="agreement"> <input type="checkbox" id="agreeCheck" checked> <label for="agreeCheck">我已阅读并同意<a href="#">《服务条款》</a>和<a href="#">《隐私政策》</a></label> </div> <button type="submit" class="login-btn" onclick="login()">登录</button> </form> <div class="footer"> <p>版权所有 ©九转苍翎</p> </div> </div> <!-- 引入jQuery依赖 --> <script src="js/jquery.min.js"></script> <script> //刷新验证码 $("#verificationCodeImg").click(function(){ //new Date().getTime()).fadeIn()防止前端缓存 $(this).hide().attr('src', '/captcha/get?dt=' + new Date().getTime()).fadeIn(); }); //登录 function login() { $.ajax({ url: '/user/login', type: "post", data:{ userName:$('#username').val(), password:$('#password').val(), captcha:$('#inputCaptcha').val(), }, success: function(result) { console.log(result); if (result.code === "SUCCESS") { alert(result.data) }else { alert(result.error) } }, }) } </script></body></html>运行本项目html3.在UserController类新增captcha形参接收来自CaptchaController类的请求,并传递给UserServiceimport jakarta.servlet.http.HttpSession;import org.example.springlogin.controller.CaptchaController;import org.example.springlogin.mapper.UserMapper;import org.example.springlogin.model.UserInfo;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Servicepublic class UserService { private final UserMapper userMapper; @Autowired public UserService(UserMapper userMapper) { this.userMapper = userMapper; } public String getUserInfoByUserName(String userName, String password, String captcha, HttpSession session) { UserInfo userInfo = userMapper.getUserInfoByUserName(userName); if (userInfo == null) { return "用户不存在"; } if (!password.equals(userInfo.getPassword())) { return "密码错误"; } long saveTime = (long)session.getAttribute("CAPTCHA_SESSION_DATE"); if (System.currentTimeMillis() - saveTime > CaptchaController.delay) { return "验证码超时"; } if (!captcha.equalsIgnoreCase((String) session.getAttribute("CAPTCHA_SESSION_CODE"))) { return "验证码错误"; } return "登录成功"; }}运行本项目java运行实现效果:5.MD5加密MD5(Message-Digest Algorithm 5)是一种广泛使用的哈希函数,可将任意长度数据生成固定长度(128位,16字节)的哈希值,通常表示为32位十六进制字符串,常用于校验数据完整性或存储密码。但因其安全性不足,通常结合盐值(Salt)配合使用不可逆性:无法通过哈希值反推原始数据唯一性:理论上不同输入产生相同哈希值的概率极低(哈希碰撞)固定长度:无论输入数据大小,输出均为32位十六进制字符串1.创建SecurityUtil类用于生成和验证密文import org.springframework.util.DigestUtils;import org.springframework.util.StringUtils;import java.util.UUID;public class SecurityUtil { //加密 public static String encrypt(String inputPassword){ //生成随机盐值 String salt = UUID.randomUUID().toString().replaceAll("-", ""); //(密码+盐值)进行加密 String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes()); return salt + finalPassword; } //验证 public static boolean verify(String inputPassword, String sqlPassword){ if (!StringUtils.hasLength(inputPassword)){ return false; } if (sqlPassword == null || sqlPassword.length() != 64){ return false; } //取出盐值 String salt = sqlPassword.substring(0,32); //(输入密码 + 盐值)重新生成 加密密码 String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes()); //判断数据库中储存的密码与输入密码是否一致 return (salt + finalPassword).equals(sqlPassword); } public static void main(String[] args) { System.out.println(SecurityUtil.encrypt("123456")); }}运行本项目java运行2.将数据库中的密码替换为加密后的值3.修改验证密码的逻辑(UserService类) if (!SecurityUtil.verify(password,userInfo.getPassword())) { return "密码错误"; }运行本项目java运行6.拦截器Spring拦截器(Interceptor)是一种基于AOP的机制,用于在请求处理的不同阶段插入自定义逻辑。常用于权限校验、日志记录、参数预处理等场景1.创建拦截器类并实现HandlerInterceptor接口,该接口提供了三种方法:preHandle:在Controller方法执行前调用postHandle:Controller方法执行后、视图渲染前调用afterCompletion:请求完成、视图渲染完毕后调用import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;@Slf4j@Componentpublic class Interceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { //1.获取token String cookie = request.getHeader("cookie"); if (cookie == null) { response.setStatus(401); return false; } log.info("接收到cookie:{}",cookie); //2.校验token return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { log.info("postHandle"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { log.info("afterCompletion"); }}运行本项目java运行2.注册拦截器import org.example.springlogin.intercepter.Interceptor;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.Arrays;import java.util.List;@Configurationpublic class Config implements WebMvcConfigurer { private final Interceptor Interceptor; @Autowired public Config(Interceptor interceptor) { Interceptor = interceptor; } //排除不需要拦截的路径 private static final List<String> excludes = Arrays.asList( "/**/login.html", "/user/login", "/captcha/get" ); @Override public void addInterceptors(InterceptorRegistry registry) { //注册拦截器 registry.addInterceptor(Interceptor) //拦截所有路径 .addPathPatterns("/**") .excludePathPatterns(excludes); }}运行本项目java运行3.创建home.html文件,并且在登录成功后跳转到该页面(在login.html中添加location.href="/home.html")<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>home</title></head><body> <h1>Hello World</h1></body></html>运行本项目html实现效果:成功登陆时未登录直接访问home.html页面时————————————————原文链接:https://blog.csdn.net/2401_89167985/article/details/153261457
-
前言在现代企业级Java应用开发中,事务管理是确保数据一致性和完整性的核心机制。Spring框架作为Java生态系统中最重要的框架之一,提供了强大而灵活的事务管理功能。本文将从基础概念出发,深入探讨Spring事务管理的各个方面,通过丰富的代码示例和实践案例,帮助开发者全面掌握Spring事务管理的精髓。无论你是刚接触Spring事务的新手,还是希望深化理解的资深开发者,本文都将为你提供有价值的见解和实用的技巧。我们将先概览Spring事务的整体架构,然后深入各个具体模块,最后进行知识总结和扩展思考。第一章:Spring事务基础概念与核心原理1.1 事务的基本概念事务(Transaction)是数据库操作的基本单位,它是一个不可分割的工作逻辑单元。事务必须满足ACID特性:原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚一致性(Consistency):事务执行前后,数据库的完整性约束没有被破坏隔离性(Isolation):并发执行的事务之间不能互相干扰持久性(Durability):事务一旦提交,其结果就是永久性的1.2 Spring事务管理架构Spring事务管理基于以下核心组件:1.2.1 PlatformTransactionManager接口public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException;}运行本项目java运行这是Spring事务管理的核心接口,定义了事务的基本操作。不同的数据访问技术有不同的实现:DataSourceTransactionManager:用于JDBC和MyBatisJpaTransactionManager:用于JPAHibernateTransactionManager:用于Hibernate1.2.2 TransactionDefinition接口public interface TransactionDefinition { int PROPAGATION_REQUIRED = 0; int PROPAGATION_SUPPORTS = 1; int PROPAGATION_MANDATORY = 2; // ... 其他传播行为常量 int getPropagationBehavior(); int getIsolationLevel(); int getTimeout(); boolean isReadOnly(); String getName();}运行本项目java运行1.3 Spring事务管理的实现原理Spring事务管理基于AOP(面向切面编程)实现,通过代理模式在方法调用前后添加事务逻辑:@Componentpublic class UserService { @Autowired private UserRepository userRepository; @Transactional public void createUser(User user) { // Spring会在此方法执行前开启事务 userRepository.save(user); // 方法正常结束时提交事务,异常时回滚 }}运行本项目java运行当Spring容器创建UserService的代理对象时,会织入事务管理逻辑:// 简化的代理逻辑示意public class UserServiceProxy extends UserService { private PlatformTransactionManager transactionManager; @Override public void createUser(User user) { TransactionStatus status = null; try { // 开启事务 status = transactionManager.getTransaction(new DefaultTransactionDefinition()); // 调用实际的业务方法 super.createUser(user); // 提交事务 transactionManager.commit(status); } catch (Exception e) { // 回滚事务 if (status != null) { transactionManager.rollback(status); } throw e; } }}运行本项目java运行第二章:Spring事务管理器详解2.1 事务管理器的选择与配置2.1.1 DataSourceTransactionManager配置对于使用JDBC或MyBatis的应用,通常选择DataSourceTransactionManager:@Configuration@EnableTransactionManagementpublic class TransactionConfig { @Bean public DataSource dataSource() { HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/testdb"); dataSource.setUsername("root"); dataSource.setPassword("password"); return dataSource; } @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }}运行本项目java运行2.1.2 JpaTransactionManager配置对于JPA应用,使用JpaTransactionManager:@Configuration@EnableTransactionManagement@EnableJpaRepositories(basePackages = "com.example.repository")public class JpaConfig { @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory( DataSource dataSource) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); em.setPackagesToScan("com.example.entity"); HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); em.setJpaVendorAdapter(vendorAdapter); Properties properties = new Properties(); properties.setProperty("hibernate.hbm2ddl.auto", "update"); properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect"); em.setJpaProperties(properties); return em; } @Bean public PlatformTransactionManager transactionManager( EntityManagerFactory entityManagerFactory) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManagerFactory); return transactionManager; }}运行本项目java运行2.2 多数据源事务管理在复杂的企业应用中,经常需要处理多个数据源的事务:@Configuration@EnableTransactionManagementpublic class MultiDataSourceConfig { @Bean @Primary public DataSource primaryDataSource() { // 主数据源配置 return DataSourceBuilder.create() .url("jdbc:mysql://localhost:3306/primary_db") .username("root") .password("password") .build(); } @Bean public DataSource secondaryDataSource() { // 从数据源配置 return DataSourceBuilder.create() .url("jdbc:mysql://localhost:3306/secondary_db") .username("root") .password("password") .build(); } @Bean @Primary public PlatformTransactionManager primaryTransactionManager( @Qualifier("primaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean public PlatformTransactionManager secondaryTransactionManager( @Qualifier("secondaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }}运行本项目java运行使用时可以通过@Transactional注解指定事务管理器:@Servicepublic class MultiDataSourceService { @Transactional("primaryTransactionManager") public void operateOnPrimaryDB() { // 操作主数据库 } @Transactional("secondaryTransactionManager") public void operateOnSecondaryDB() { // 操作从数据库 }}运行本项目java运行2.3 分布式事务管理对于跨多个资源的分布式事务,Spring提供了JTA支持:@Configuration@EnableTransactionManagementpublic class JtaConfig { @Bean public JtaTransactionManager transactionManager() { JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); jtaTransactionManager.setUserTransaction(userTransaction()); jtaTransactionManager.setTransactionManager(atomikosTransactionManager()); return jtaTransactionManager; } @Bean public UserTransaction userTransaction() throws SystemException { UserTransactionImp userTransactionImp = new UserTransactionImp(); userTransactionImp.setTransactionTimeout(300); return userTransactionImp; } @Bean public TransactionManager atomikosTransactionManager() { UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(false); return userTransactionManager; }}运行本项目java运行第三章:声明式事务配置与使用3.1 @Transactional注解详解@Transactional是Spring声明式事务的核心注解,提供了丰富的配置选项:@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface Transactional { String value() default ""; String transactionManager() default ""; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {};}运行本项目java运行3.2 注解使用最佳实践3.2.1 类级别与方法级别注解@Service@Transactional(readOnly = true) // 类级别默认只读事务public class UserService { @Autowired private UserRepository userRepository; // 继承类级别的只读事务 public List<User> findAllUsers() { return userRepository.findAll(); } // 方法级别覆盖类级别配置 @Transactional(readOnly = false, rollbackFor = Exception.class) public User createUser(User user) { validateUser(user); return userRepository.save(user); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateUserStatus(Long userId, UserStatus status) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found")); user.setStatus(status); userRepository.save(user); } private void validateUser(User user) { if (user.getEmail() == null || user.getEmail().isEmpty()) { throw new IllegalArgumentException("Email cannot be empty"); } }}运行本项目java运行3.2.2 异常处理与回滚配置@Servicepublic class OrderService { @Autowired private OrderRepository orderRepository; @Autowired private InventoryService inventoryService; @Autowired private PaymentService paymentService; // 默认只对RuntimeException和Error回滚 @Transactional public Order createOrder(OrderRequest request) { Order order = new Order(request); orderRepository.save(order); // 如果库存不足,抛出RuntimeException,事务会回滚 inventoryService.reserveItems(request.getItems()); return order; } // 指定对所有异常都回滚 @Transactional(rollbackFor = Exception.class) public void processPayment(Long orderId, PaymentInfo paymentInfo) throws PaymentException { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException("Order not found")); try { paymentService.processPayment(paymentInfo); order.setStatus(OrderStatus.PAID); orderRepository.save(order); } catch (PaymentException e) { // PaymentException是检查异常,但配置了rollbackFor = Exception.class // 所以事务会回滚 throw e; } } // 指定某些异常不回滚 @Transactional(noRollbackFor = {BusinessException.class}) public void updateOrderWithBusinessLogic(Long orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new OrderNotFoundException("Order not found")); try { // 执行业务逻辑 performBusinessLogic(order); orderRepository.save(order); } catch (BusinessException e) { // BusinessException不会导致事务回滚 // 但数据库操作仍然会提交 log.warn("Business logic failed, but transaction will commit", e); } }}运行本项目java运行3.3 XML配置方式虽然注解方式更加流行,但XML配置在某些场景下仍然有用:<!-- applicationContext.xml --><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 数据源配置 --> <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/testdb"/> <property name="username" value="root"/> <property name="password" value="password"/> </bean> <!-- 事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 事务通知 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="find*" read-only="true"/> <tx:method name="get*" read-only="true"/> <tx:method name="*" rollback-for="Exception"/> </tx:attributes> </tx:advice> <!-- AOP配置 --> <aop:config> <aop:pointcut id="serviceOperation" expression="execution(* com.example.service.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/> </aop:config></beans>运行本项目xml第四章:编程式事务管理4.1 TransactionTemplate使用TransactionTemplate提供了编程式事务管理的模板方法:@Servicepublic class ProgrammaticTransactionService { @Autowired private TransactionTemplate transactionTemplate; @Autowired private UserRepository userRepository; @Autowired private OrderRepository orderRepository; public User createUserWithOrder(User user, Order order) { return transactionTemplate.execute(status -> { try { // 保存用户 User savedUser = userRepository.save(user); // 设置订单的用户ID order.setUserId(savedUser.getId()); // 保存订单 orderRepository.save(order); return savedUser; } catch (Exception e) { // 手动标记回滚 status.setRollbackOnly(); throw new RuntimeException("Failed to create user with order", e); } }); } public void batchUpdateUsers(List<User> users) { transactionTemplate.execute(status -> { for (User user : users) { try { userRepository.save(user); } catch (Exception e) { log.error("Failed to update user: {}", user.getId(), e); // 可以选择继续处理其他用户,或者回滚整个事务 // status.setRollbackOnly(); } } return null; }); }}运行本项目java运行4.2 PlatformTransactionManager直接使用对于更细粒度的控制,可以直接使用PlatformTransactionManager:@Servicepublic class LowLevelTransactionService { @Autowired private PlatformTransactionManager transactionManager; @Autowired private UserRepository userRepository; public void complexBusinessOperation() { // 定义事务属性 DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); def.setTimeout(30); TransactionStatus status = transactionManager.getTransaction(def); try { // 第一阶段操作 performPhaseOne(); // 检查点 - 可以根据业务逻辑决定是否继续 if (!shouldContinue()) { transactionManager.rollback(status); return; } // 第二阶段操作 performPhaseTwo(); // 提交事务 transactionManager.commit(status); } catch (Exception e) { // 回滚事务 transactionManager.rollback(status); throw new RuntimeException("Business operation failed", e); } } private void performPhaseOne() { // 第一阶段业务逻辑 } private void performPhaseTwo() { // 第二阶段业务逻辑 } private boolean shouldContinue() { // 业务判断逻辑 return true; }}运行本项目java运行4.3 编程式事务的嵌套使用@Servicepublic class NestedTransactionService { @Autowired private PlatformTransactionManager transactionManager; public void parentOperation() { DefaultTransactionDefinition parentDef = new DefaultTransactionDefinition(); parentDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus parentStatus = transactionManager.getTransaction(parentDef); try { // 父事务操作 performParentOperation(); // 调用子事务 childOperation(); transactionManager.commit(parentStatus); } catch (Exception e) { transactionManager.rollback(parentStatus); throw e; } } private void childOperation() { DefaultTransactionDefinition childDef = new DefaultTransactionDefinition(); childDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); TransactionStatus childStatus = transactionManager.getTransaction(childDef); try { // 子事务操作(独立的事务) performChildOperation(); transactionManager.commit(childStatus); } catch (Exception e) { transactionManager.rollback(childStatus); // 子事务失败不影响父事务(因为使用了REQUIRES_NEW) log.error("Child operation failed", e); } }}运行本项目java运行第五章:事务传播行为和隔离级别深度解析5.1 事务传播行为详解Spring定义了7种事务传播行为,每种都有其特定的使用场景:5.1.1 PROPAGATION_REQUIRED(默认)@Servicepublic class PropagationRequiredService { @Autowired private UserService userService; @Transactional(propagation = Propagation.REQUIRED) public void outerMethod() { // 开启新事务T1 performOperation1(); // 调用内部方法,加入事务T1 userService.innerMethod(); performOperation2(); // 如果任何操作失败,整个事务T1回滚 }}@Servicepublic class UserService { @Transactional(propagation = Propagation.REQUIRED) public void innerMethod() { // 加入外部事务T1,不会创建新事务 performUserOperation(); }}运行本项目java运行5.1.2 PROPAGATION_REQUIRES_NEW@Servicepublic class PropagationRequiresNewService { @Autowired private AuditService auditService; @Transactional public void businessOperation() { try { // 主业务逻辑在事务T1中 performMainBusiness(); // 审计日志使用独立事务T2 auditService.logOperation("Business operation completed"); } catch (Exception e) { // 即使主业务失败,审计日志也会保存(因为是独立事务) auditService.logError("Business operation failed: " + e.getMessage()); throw e; } }}@Servicepublic class AuditService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void logOperation(String message) { // 创建新事务T2,独立于调用者的事务 AuditLog log = new AuditLog(message, new Date()); auditRepository.save(log); // T2独立提交,不受T1影响 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void logError(String errorMessage) { // 错误日志也使用独立事务,确保一定会保存 ErrorLog errorLog = new ErrorLog(errorMessage, new Date()); errorRepository.save(errorLog); }}运行本项目java运行5.1.3 PROPAGATION_NESTED@Servicepublic class PropagationNestedService { @Autowired private OrderService orderService; @Transactional public void processOrderBatch(List<OrderRequest> orders) { for (OrderRequest orderRequest : orders) { try { // 每个订单处理使用嵌套事务 orderService.processOrder(orderRequest); } catch (Exception e) { // 单个订单失败不影响其他订单 log.error("Failed to process order: {}", orderRequest.getId(), e); } } }}@Servicepublic class OrderService { @Transactional(propagation = Propagation.NESTED) public void processOrder(OrderRequest request) { // 创建嵌套事务(保存点) Order order = new Order(request); orderRepository.save(order); // 如果这里抛出异常,只回滚到保存点 // 外部事务可以继续执行 validateAndProcessPayment(order); }}运行本项目java运行5.1.4 其他传播行为示例@Servicepublic class OtherPropagationService { // PROPAGATION_SUPPORTS:支持当前事务,如果没有事务则以非事务方式执行 @Transactional(propagation = Propagation.SUPPORTS) public List<User> findUsers() { // 如果在事务中调用,加入事务 // 如果不在事务中调用,以非事务方式执行 return userRepository.findAll(); } // PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务则挂起 @Transactional(propagation = Propagation.NOT_SUPPORTED) public void performNonTransactionalOperation() { // 总是以非事务方式执行 // 如果当前有事务,会被挂起 performLongRunningOperation(); } // PROPAGATION_MANDATORY:必须在事务中执行,否则抛出异常 @Transactional(propagation = Propagation.MANDATORY) public void mandatoryTransactionMethod() { // 如果没有活动事务,抛出IllegalTransactionStateException performCriticalOperation(); } // PROPAGATION_NEVER:不能在事务中执行,否则抛出异常 @Transactional(propagation = Propagation.NEVER) public void neverInTransactionMethod() { // 如果当前有活动事务,抛出IllegalTransactionStateException performIndependentOperation(); }}运行本项目java运行5.2 事务隔离级别详解5.2.1 隔离级别对比@Servicepublic class IsolationLevelService { // READ_UNCOMMITTED:最低隔离级别,可能出现脏读 @Transactional(isolation = Isolation.READ_UNCOMMITTED) public List<User> readUncommittedExample() { // 可能读取到其他事务未提交的数据 return userRepository.findAll(); } // READ_COMMITTED:防止脏读,但可能出现不可重复读 @Transactional(isolation = Isolation.READ_COMMITTED) public User readCommittedExample(Long userId) { User user1 = userRepository.findById(userId).orElse(null); // 在这期间,其他事务可能修改了用户数据 performSomeOperation(); User user2 = userRepository.findById(userId).orElse(null); // user1和user2可能不同(不可重复读) return user2; } // REPEATABLE_READ:防止脏读和不可重复读,但可能出现幻读 @Transactional(isolation = Isolation.REPEATABLE_READ) public List<User> repeatableReadExample() { List<User> users1 = userRepository.findByStatus(UserStatus.ACTIVE); performSomeOperation(); List<User> users2 = userRepository.findByStatus(UserStatus.ACTIVE); // users1和users2中的现有记录相同,但users2可能包含新插入的记录(幻读) return users2; } // SERIALIZABLE:最高隔离级别,防止所有并发问题 @Transactional(isolation = Isolation.SERIALIZABLE) public void serializableExample() { // 完全串行化执行,性能最低但数据一致性最高 List<User> users = userRepository.findAll(); for (User user : users) { user.setLastAccessTime(new Date()); userRepository.save(user); } }}运行本项目java运行5.2.2 隔离级别实际应用场景@Servicepublic class BankingService { @Autowired private AccountRepository accountRepository; // 转账操作需要高隔离级别确保数据一致性 @Transactional(isolation = Isolation.SERIALIZABLE) public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) { Account fromAccount = accountRepository.findById(fromAccountId) .orElseThrow(() -> new AccountNotFoundException("From account not found")); Account toAccount = accountRepository.findById(toAccountId) .orElseThrow(() -> new AccountNotFoundException("To account not found")); if (fromAccount.getBalance().compareTo(amount) < 0) { throw new InsufficientFundsException("Insufficient funds"); } fromAccount.setBalance(fromAccount.getBalance().subtract(amount)); toAccount.setBalance(toAccount.getBalance().add(amount)); accountRepository.save(fromAccount); accountRepository.save(toAccount); } // 查询余额可以使用较低的隔离级别 @Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true) public BigDecimal getBalance(Long accountId) { Account account = accountRepository.findById(accountId) .orElseThrow(() -> new AccountNotFoundException("Account not found")); return account.getBalance(); } // 生成对账单可以使用REPEATABLE_READ确保数据一致性 @Transactional(isolation = Isolation.REPEATABLE_READ, readOnly = true) public AccountStatement generateStatement(Long accountId, Date startDate, Date endDate) { Account account = accountRepository.findById(accountId) .orElseThrow(() -> new AccountNotFoundException("Account not found")); List<Transaction> transactions = transactionRepository .findByAccountIdAndDateBetween(accountId, startDate, endDate); return new AccountStatement(account, transactions, startDate, endDate); }}运行本项目java运行第六章:Spring事务最佳实践和常见问题6.1 事务使用最佳实践6.1.1 事务边界设计// 错误示例:事务边界过大@Servicepublic class BadTransactionService { @Transactional public void processLargeDataSet(List<Data> dataList) { for (Data data : dataList) { // 长时间运行的操作 processComplexData(data); // 外部服务调用 externalService.sendNotification(data); // 文件操作 fileService.writeToFile(data); } // 事务持续时间过长,容易导致锁竞争和超时 }}// 正确示例:合理的事务边界@Servicepublic class GoodTransactionService { public void processLargeDataSet(List<Data> dataList) { for (Data data : dataList) { try { // 每个数据项使用独立事务 processSingleData(data); } catch (Exception e) { log.error("Failed to process data: {}", data.getId(), e); // 单个失败不影响其他数据处理 } } } @Transactional public void processSingleData(Data data) { // 只包含数据库操作的事务 dataRepository.save(processData(data)); // 非事务操作放在事务外 CompletableFuture.runAsync(() -> { externalService.sendNotification(data); fileService.writeToFile(data); }); }}运行本项目java运行6.1.2 只读事务优化@Servicepublic class OptimizedReadService { // 明确标记只读事务 @Transactional(readOnly = true) public List<User> findActiveUsers() { return userRepository.findByStatus(UserStatus.ACTIVE); } // 复杂查询使用只读事务 @Transactional(readOnly = true) public UserStatistics generateUserStatistics() { long totalUsers = userRepository.count(); long activeUsers = userRepository.countByStatus(UserStatus.ACTIVE); long inactiveUsers = userRepository.countByStatus(UserStatus.INACTIVE); return new UserStatistics(totalUsers, activeUsers, inactiveUsers); } // 分页查询优化 @Transactional(readOnly = true) public Page<User> findUsersWithPagination(Pageable pageable) { return userRepository.findAll(pageable); }}运行本项目java运行6.1.3 异常处理策略@Servicepublic class ExceptionHandlingService { @Transactional(rollbackFor = Exception.class) public void robustBusinessOperation(BusinessRequest request) { try { // 主要业务逻辑 performMainOperation(request); } catch (ValidationException e) { // 验证异常,记录日志但不回滚 log.warn("Validation failed: {}", e.getMessage()); throw new BusinessException("Invalid request", e); } catch (ExternalServiceException e) { // 外部服务异常,可能需要重试 log.error("External service failed: {}", e.getMessage()); // 标记为需要重试 markForRetry(request); throw e; } catch (Exception e) { // 其他异常,记录详细信息 log.error("Unexpected error in business operation", e); throw new SystemException("System error occurred", e); } } // 使用自定义异常控制回滚行为 @Transactional(rollbackFor = {DataIntegrityException.class}, noRollbackFor = {BusinessWarningException.class}) public void selectiveRollbackOperation() { try { performDataOperation(); } catch (BusinessWarningException e) { // 业务警告不回滚事务,但记录警告 log.warn("Business warning: {}", e.getMessage()); } }}运行本项目java运行6.2 常见问题与解决方案6.2.1 事务失效问题// 问题:内部方法调用导致事务失效@Servicepublic class TransactionFailureService { public void publicMethod() { // 直接调用内部方法,@Transactional不会生效 this.internalTransactionalMethod(); } @Transactional private void internalTransactionalMethod() { // 事务不会生效,因为是内部调用 performDatabaseOperation(); }}// 解决方案1:使用自注入@Servicepublic class SelfInjectionService { @Autowired private SelfInjectionService self; public void publicMethod() { // 通过代理对象调用,事务生效 self.internalTransactionalMethod(); } @Transactional public void internalTransactionalMethod() { performDatabaseOperation(); }}// ✅ 解决方案2:拆分到不同的Service@Servicepublic class CallerService { @Autowired private TransactionalService transactionalService; public void publicMethod() { transactionalService.transactionalMethod(); }}@Servicepublic class TransactionalService { @Transactional public void transactionalMethod() { performDatabaseOperation(); }}运行本项目java运行6.2.2 事务超时处理@Servicepublic class TimeoutHandlingService { // 设置合理的超时时间 @Transactional(timeout = 30) // 30秒超时 public void normalOperation() { performQuickOperation(); } // 长时间运行的操作需要更长的超时时间 @Transactional(timeout = 300) // 5分钟超时 public void longRunningOperation() { performBatchOperation(); } // 对于可能很长的操作,考虑分批处理 public void processLargeDataset(List<Data> dataList) { int batchSize = 100; for (int i = 0; i < dataList.size(); i += batchSize) { List<Data> batch = dataList.subList(i, Math.min(i + batchSize, dataList.size())); processBatch(batch); } } @Transactional(timeout = 60) private void processBatch(List<Data> batch) { for (Data data : batch) { dataRepository.save(data); } }}运行本项目java运行6.2.3 死锁预防@Servicepublic class DeadlockPreventionService { // 按固定顺序获取锁,避免死锁 @Transactional public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) { // 确保按ID顺序获取锁 Long firstId = Math.min(fromAccountId, toAccountId); Long secondId = Math.max(fromAccountId, toAccountId); Account firstAccount = accountRepository.findByIdForUpdate(firstId); Account secondAccount = accountRepository.findByIdForUpdate(secondId); Account fromAccount = fromAccountId.equals(firstId) ? firstAccount : secondAccount; Account toAccount = toAccountId.equals(firstId) ? firstAccount : secondAccount; // 执行转账逻辑 performTransfer(fromAccount, toAccount, amount); } // 使用乐观锁避免死锁 @Transactional public void updateAccountWithOptimisticLock(Long accountId, BigDecimal amount) { int maxRetries = 3; int retryCount = 0; while (retryCount < maxRetries) { try { Account account = accountRepository.findById(accountId) .orElseThrow(() -> new AccountNotFoundException("Account not found")); account.setBalance(account.getBalance().add(amount)); accountRepository.save(account); return; // 成功,退出重试循环 } catch (OptimisticLockingFailureException e) { retryCount++; if (retryCount >= maxRetries) { throw new ConcurrencyException("Failed to update account after " + maxRetries + " retries"); } // 短暂等待后重试 try { Thread.sleep(100 * retryCount); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("Thread interrupted", ie); } } } }}运行本项目java运行6.3 性能优化技巧6.3.1 批量操作优化@Servicepublic class BatchOptimizationService { // 低效的逐条处理 @Transactional public void inefficientBatchInsert(List<User> users) { for (User user : users) { userRepository.save(user); // 每次都执行SQL } } // 高效的批量处理 @Transactional public void efficientBatchInsert(List<User> users) { int batchSize = 50; for (int i = 0; i < users.size(); i += batchSize) { List<User> batch = users.subList(i, Math.min(i + batchSize, users.size())); userRepository.saveAll(batch); // 每批次后清理持久化上下文 if (i % batchSize == 0) { entityManager.flush(); entityManager.clear(); } } } // 使用JDBC批量操作 @Transactional public void jdbcBatchInsert(List<User> users) { String sql = "INSERT INTO users (name, email, status) VALUES (?, ?, ?)"; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { User user = users.get(i); ps.setString(1, user.getName()); ps.setString(2, user.getEmail()); ps.setString(3, user.getStatus().name()); } @Override public int getBatchSize() { return users.size(); } }); }}运行本项目java运行6.3.2 连接池配置优化@Configurationpublic class DataSourceOptimizationConfig { @Bean public DataSource optimizedDataSource() { HikariConfig config = new HikariConfig(); // 基本连接配置 config.setJdbcUrl("jdbc:mysql://localhost:3306/testdb"); config.setUsername("root"); config.setPassword("password"); // 连接池优化配置 config.setMaximumPoolSize(20); // 最大连接数 config.setMinimumIdle(5); // 最小空闲连接数 config.setConnectionTimeout(30000); // 连接超时时间 config.setIdleTimeout(600000); // 空闲连接超时时间 config.setMaxLifetime(1800000); // 连接最大生存时间 // 性能优化配置 config.setLeakDetectionThreshold(60000); // 连接泄漏检测 config.addDataSourceProperty("cachePrepStmts", "true"); config.addDataSourceProperty("prepStmtCacheSize", "250"); config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); return new HikariDataSource(config); }}运行本项目java运行第七章:总结与展望7.1 核心知识点回顾通过本文的深入探讨,我们全面了解了Spring事务管理的各个方面:7.1.1 基础概念掌握ACID特性:原子性、一致性、隔离性、持久性是事务的基本保证Spring事务架构:PlatformTransactionManager、TransactionDefinition、TransactionStatus三大核心接口AOP实现原理:基于代理模式的声明式事务管理机制7.1.2 实践技能提升声明式事务:@Transactional注解的灵活使用和最佳实践编程式事务:TransactionTemplate和PlatformTransactionManager的直接使用传播行为:7种传播行为的适用场景和实际应用隔离级别:4种隔离级别的性能与一致性权衡7.1.3 问题解决能力常见陷阱:事务失效、死锁、超时等问题的识别和解决性能优化:批量操作、连接池配置、事务边界设计最佳实践:异常处理、只读事务、多数据源管理7.2 进阶学习路径7.2.1 深入源码研究// 建议研究的核心类// 1. AbstractPlatformTransactionManager - 事务管理器抽象实现// 2. TransactionInterceptor - 事务拦截器// 3. TransactionAspectSupport - 事务切面支持// 4. DefaultTransactionStatus - 事务状态实现// 示例:自定义事务管理器public class CustomTransactionManager extends AbstractPlatformTransactionManager { @Override protected Object doGetTransaction() throws TransactionException { // 获取事务对象的实现 return new CustomTransactionObject(); } @Override protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { // 开始事务的实现 } @Override protected void doCommit(DefaultTransactionStatus status) throws TransactionException { // 提交事务的实现 } @Override protected void doRollback(DefaultTransactionStatus status) throws TransactionException { // 回滚事务的实现 }}运行本项目java运行7.2.2 分布式事务探索// Seata分布式事务示例@GlobalTransactionalpublic class DistributedTransactionService { @Autowired private OrderService orderService; @Autowired private PaymentService paymentService; @Autowired private InventoryService inventoryService; public void createOrder(OrderRequest request) { // 创建订单 Order order = orderService.createOrder(request); // 扣减库存 inventoryService.deductInventory(request.getItems()); // 处理支付 paymentService.processPayment(order.getId(), request.getPaymentInfo()); // 如果任何服务失败,全局事务会回滚 }}运行本项目java运行7.2.3 响应式事务管理// Spring WebFlux响应式事务@Servicepublic class ReactiveTransactionService { @Autowired private ReactiveTransactionManager transactionManager; public Mono<User> createUserReactive(User user) { return transactionManager.execute(status -> { return userRepository.save(user) .doOnError(error -> status.setRollbackOnly()); }); }}运行本项目java运行7.3 扩展阅读建议7.3.1 官方文档与规范Spring Framework Reference Documentation - Transaction ManagementJDBC Transaction Management7.3.2 深度技术文章《Spring事务管理源码深度解析》《分布式事务解决方案对比》《数据库事务隔离级别实现原理》7.3.3 相关技术栈MyBatis事务集成:深入了解MyBatis与Spring事务的集成机制JPA事务管理:掌握JPA规范下的事务管理特性分布式事务框架:Seata、Saga、TCC等分布式事务解决方案7.4 实践项目建议7.4.1 基础练习项目// 项目1:银行转账系统// 功能要求:// 1. 实现账户间转账功能// 2. 支持并发转账处理// 3. 实现转账记录和审计日志// 4. 处理各种异常情况@Servicepublic class BankTransferProject { @Transactional(isolation = Isolation.SERIALIZABLE) public TransferResult transfer(TransferRequest request) { // 实现转账逻辑 // 考虑并发控制、异常处理、审计日志等 }}运行本项目java运行7.4.2 进阶挑战项目// 项目2:电商订单系统// 功能要求:// 1. 订单创建涉及多个服务(库存、支付、物流)// 2. 实现分布式事务管理// 3. 支持订单状态机和补偿机制// 4. 性能优化和监控@GlobalTransactionalpublic class ECommerceOrderProject { public OrderResult createOrder(OrderRequest request) { // 实现复杂的订单创建流程 // 涉及多个微服务的协调 }}运行本项目java运行7.5 技术发展趋势7.5.1 云原生事务管理随着微服务和云原生架构的普及,事务管理正朝着以下方向发展:Saga模式:长事务的分解和补偿机制事件驱动架构:基于事件的最终一致性服务网格集成:Istio等服务网格中的事务管理7.5.2 响应式编程支持Spring WebFlux和响应式编程的兴起带来了新的事务管理需求:非阻塞事务:基于响应式流的事务处理背压处理:在事务中处理背压和流控异步事务协调:跨异步边界的事务管理7.6 讨论与思考7.6.1 开放性问题性能vs一致性:在高并发场景下,如何平衡事务的性能和数据一致性?微服务事务:在微服务架构中,是否应该避免跨服务事务?有哪些替代方案?事务边界设计:如何设计合理的事务边界来平衡业务完整性和系统性能?7.6.2 实践挑战遗留系统改造:如何将传统的事务管理代码迁移到Spring事务管理?测试策略:如何有效测试事务相关的代码,特别是异常场景?监控和诊断:如何监控事务性能和诊断事务相关问题?7.7 结语Spring事务管理是企业级Java开发中的核心技能,掌握它不仅需要理解理论知识,更需要在实践中不断积累经验。本文提供了从基础到高级的全面指南,但技术的学习永无止境。希望读者能够:持续实践:在实际项目中应用所学知识深入研究:探索更深层次的实现原理分享交流:与同行分享经验和最佳实践关注发展:跟上技术发展的最新趋势记住,优秀的事务管理不仅仅是技术实现,更是对业务逻辑的深刻理解和对系统架构的整体把握。让我们在Spring事务管理的道路上持续前进,构建更加健壮、高效的企业级应用————————————————原文链接:https://blog.csdn.net/weixin_63944437/article/details/153634279
-
引言MQTT(Message Queuing Telemetry Transport)是一种轻量级的消息传输协议,专为低带宽、高延迟或不稳定的网络环境设计。它广泛应用于物联网(IoT)、机器对机器(M2M)通信以及实时数据传输等领域。本文将详细介绍如何在 Java 中使用 MQTT 协议实现高效的消息传递。为什么选择 MQTT?MQTT 具有以下优点:轻量级:MQTT 协议头非常小,减少了网络带宽的占用。低延迟:MQTT 支持发布/订阅模式,消息传递速度快。可靠性:MQTT 提供了三种服务质量(QoS)级别,确保消息的可靠传递。灵活性:MQTT 支持保留消息、遗嘱消息等功能,满足不同场景的需求。MQTT 基础概念在深入 Java 实现之前,我们需要了解一些 MQTT 的基本概念:Broker:消息代理,负责接收发布者发送的消息并将其分发给订阅者。Publisher:消息发布者,负责将消息发送到 Broker。Subscriber:消息订阅者,负责从 Broker 接收消息。Topic:消息的主题,订阅者通过订阅特定的主题来接收消息。QoS:服务质量级别,MQTT 提供了 0、1、2 三个级别,分别代表不同的可靠性。在 Java 中使用 MQTT要在 Java 中使用 MQTT,我们可以使用 Eclipse Paho 库。Paho 是一个开源的 MQTT 客户端库,支持多种编程语言,包括 Java。1. 添加依赖首先,在项目的 pom.xml 文件中添加 Paho MQTT 客户端的依赖:代码语言:xmlAI代码解释<dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId> <version>1.2.5</version></dependency>AI写代码2. 创建 MQTT 客户端接下来,我们创建一个 MQTT 客户端实例,并连接到 MQTT Broker:代码语言:javaAI代码解释import org.eclipse.paho.client.mqttv3.*;import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; public class MqttExample { public static void main(String[] args) { String broker = "tcp://broker.hivemq.com:1883"; String clientId = "JavaSample"; MemoryPersistence persistence = new MemoryPersistence(); try { MqttClient client = new MqttClient(broker, clientId, persistence); MqttConnectOptions connOpts = new MqttConnectOptions(); connOpts.setCleanSession(true); System.out.println("Connecting to broker: " + broker); client.connect(connOpts); System.out.println("Connected"); // 订阅主题 String topic = "test/topic"; client.subscribe(topic); // 发布消息 String content = "Hello MQTT"; int qos = 2; MqttMessage message = new MqttMessage(content.getBytes()); message.setQos(qos); client.publish(topic, message); // 断开连接 client.disconnect(); System.out.println("Disconnected"); } catch (MqttException me) { System.out.println("reason " + me.getReasonCode()); System.out.println("msg " + me.getMessage()); System.out.println("loc " + me.getLocalizedMessage()); System.out.println("cause " + me.getCause()); System.out.println("excep " + me); me.printStackTrace(); } }}AI写代码3. 处理消息回调为了接收和处理从 Broker 发送的消息,我们需要实现 MqttCallback 接口,并将其设置为 MQTT 客户端的回调:代码语言:javaAI代码解释import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;import org.eclipse.paho.client.mqttv3.MqttCallback;import org.eclipse.paho.client.mqttv3.MqttMessage; public class MqttExample implements MqttCallback { @Override public void connectionLost(Throwable cause) { System.out.println("Connection lost"); cause.printStackTrace(); } @Override public void messageArrived(String topic, MqttMessage message) throws Exception { System.out.println("Message arrived: " + new String(message.getPayload())); } @Override public void deliveryComplete(IMqttDeliveryToken token) { System.out.println("Delivery complete"); } // 其他代码...}AI写代码4. 运行示例将上述代码整合到一个完整的 Java 类中,并运行该类。你将看到以下输出:代码语言:javaAI代码解释Connecting to broker: tcp://broker.hivemq.com:1883ConnectedMessage arrived: Hello MQTTDisconnectedAI写代码总结通过本文,我们了解了如何在 Java 中使用 MQTT 协议实现高效的消息传递。我们介绍了 MQTT 的基本概念,展示了如何使用 Eclipse Paho 库创建 MQTT 客户端,订阅主题,发布消息以及处理消息回调。MQTT 在物联网和实时数据传输领域具有广泛的应用前景。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/weixin_43044226/article/details/153143643
-
1.前置知识1.1 Tomcat定义:Tomcat是一个开源的轻量级Web(Http)服务器和Servlet容器。它实现了Java Servlet等Java EE规范的核心功能,常用于部署和运行Java Web应用程序 。换言之,Tomcat就是一个严格遵循Servlet规范开发出来的、可以独立安装和运行的Java Web服务器/Servlet容器核心功能:Servlet容器:支持Servlet的执行,处理HTTP请求和响应Web服务器:提供静态资源(如HTML)的访问能力,支持基本的HTTP服务安装与版本对应:tomcat官网:Apache Tomcat®目录结构:bin:存放可执行文件,如startup.batconf:存放配置文件lib:存放Tomcat运行所需的jar文件logs:存储日志文件temp:存放临时文件,如上传的文件或缓存数据webapps:默认web应用部署目录work:服务器的工作目录,存放运行时生成的临时文件(编译文件)1.2 Servlet1.2.1 定义Servlet是Java语言编写的、运行在服务器端的程序,它遵循一套标准的API规范(Tomcat是这套规范的一个具体实现/容器,并提供了让Servlet与前端交互的运行时环境)1.2.2 API示范创建项目/配置文件:(1)在IEDA中创建Maven项目(2)在pom.xml文件中添加servlet依赖(置于< project >< /project >标签下)<dependencies> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <!--servlet依赖版本应与jdk和tomcat的版本相匹配--> <version>6.1.0</version> <scope>provided</scope> </dependency></dependencies>(3)在main路径下创建webapp/Web-INF/web.xml,在xml文件中添加以下内容<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" ><web-app> <display-name>Archetype Created Web Application</display-name></web-app>(4)下载插件:Smart Tomcat(为了方便启动项目)API示例:import jakarta.servlet.annotation.WebServlet;import jakarta.servlet.http.HttpServlet;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;//设置访问路径(url)@WebServlet("/method")//继承HttpServlet并重写doGet和doPost方法public class MethodServlet extends HttpServlet { //接收method=post的请求 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doPost"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doPost"); } //接收method=put的请求 @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doPut"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doPut"); } //接收method=delete的请求 @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { System.out.println("doDelete"); resp.setContentType("text/html; charset=utf-8"); resp.getWriter().write("doDelete"); }}1.2.3 生命周期定义:Servlet 生命周期由 Web容器(如Tomcat)管理,包含加载、初始化、处理请求和销毁四个阶段。每个阶段对应特定的方法调用,开发者可通过重写这些方法实现自定义逻辑1.类加载:Web容器通过类加载器加载 Servlet 类(通常首次请求触发或容器启动时预加载,字节码文件被加载到内存但未实例化)。具体类加载流程请阅读:Java虚拟机——JVM(JavaVirtualMachine)解析一1.5 实例化:确认Servlet类成功加载后立刻执行,在整个Web容器中每种Servlet类(如HttpServlet)只会有一个实例化对象2.初始化:Web容器调用刚刚创建好的Servlet实例的init(ServletConfig config)方法,在整个servlet实例的生命周期中仅调用一次,主要作用是读取配置和资源加载。若初始化失败,抛出ServletException,Servlet不会被加入可用队列3.处理请求:Web容器为每个请求创建线程,调用service(ServletRequest req, ServletResponse res)方法。service() 方法根据HTTP请求类型(get/post)调用doGet()或doPost()4.销毁:Web容器调用 destroy()方法,Servlet 实例被标记为垃圾回收2.SpringBootServlet是Java EE规范中处理Web请求的核心组件,但随着应用复杂度提升,Servlet的直接使用显得笨重。Spring框架通过一系列抽象和扩展,简化了企业级应用开发我们可以用一个非常形象的比喻来贯穿始终:建造一座房子第一阶段:Servlet 时代 - 自己烧砖砌墙目标: 建造一个能遮风挡雨的房子(一个能处理HTTP请求的Web应用)你的工作状态:材料: 你有最基础的原材料——泥土(Java 语言)和水(JVM)。你需要自己烧制砖块(编写Servlet类)工具: 只有简单的泥瓦刀(Servlet API)过程:1.你为每一面墙、每一扇门都亲手烧制一块特定的砖(编写 LoginServlet, UserServlet, OrderServlet)2.你亲自规划每块砖的位置(在web.xml中配置大量的 < servlet > 和 < servlet-mapping >)3.你亲自搅拌水泥,一块一块地砌墙(在每个 Servlet 的doGet/doPost 方法中手动解析参数、处理业务、组装 HTML)核心特点:高度灵活: 你可以造出任何形状的砖极其繁琐: 大量重复性劳动(每个 Servlet 都有获取参数、关闭连接等样板代码)难以维护: 砖块之间紧密耦合(对象依赖硬编码),想换一扇窗(修改功能)可能会牵动整面墙依赖外部: 房子建在别人的地上(需要将war包部署到外部的Tomcat服务器)总结:Servlet提供了Web开发的基础能力,但开发效率极低,代码冗余且难以维护第二阶段:Spring 时代 - 使用预制件和设计图纸目标: 用更高效、更标准化的方式建造一个结构更好、更易扩展的房子你的工作状态:材料: 你不再烧砖,而是使用工厂提供的标准化预制件(Spring Bean,如 @Controller, @Service)核心创新: 你聘请了一位神奇的管家(IoC 容器)你不再亲自“砌砖”(用new实例化对象),只需告诉管家你需要什么(用@Autowired声明依赖)管家会自动把预制件(Bean)按照图纸(配置)组装好,送到你手上(依赖注入DI)过程:1.一个总大门(DispatcherServlet): 房子只有一个入口,所有访客(请求)都先到这里2.管家调度: 总大门处的接待员(DispatcherServlet)根据访客需求,呼叫房子里对应的专业房间(@Controller中的方法)来接待3.开发者只需专注于房间内的专业服务(业务逻辑),而不用关心访客是怎么进来的核心特点:解耦: 预制件之间是松耦合的,易于更换和测试专业化: AOP(面向切面编程)可以像“装修队”一样,非侵入式地为所有房间统一安装中央空调(日志、安全、事务)效率提升: 避免了大量重复劳动,结构清晰配置复杂: 绘制详细的“组装图纸”(配置 Spring)本身成了一项复杂的工作总结:Spring框架通过IoC/DI和AOP等理念,解决了代码耦合和重复劳动问题,但引入了显著的配置复杂度Spring Boot 1.0.0正式发布于2014年4月1日,标志着该框架的首次稳定版本发布。SpringBoot基于SpringFramework 4进行设计,显著减少了开发者的配置工作量,彻底消除了Spring的配置地狱约定大于配置:约定了默认配置Start机制:是一种依赖管理机制,每个Starter包含特定功能所需的依赖库和自动配置类,开发者只需引入对应Starter即可快速启用功能模块嵌入式容器:内置了Tomcat等嵌入式容器,无需部署war文件到外部容器,直接运行即可启动应用3.Spring Web MVC3.1 概述官方描述:Spring Web MVC是基于Servlet API构建的原始Web框架,并从一开始就在 Spring框架中。正式名称“Spring Web MVC”, 来自其源模块的名称(spring-webmvc),但它通常被称为“Spring MVC”MVC的起源与发展:MVC(Model-View-Controller)模式最初由挪威计算机科学家Trygve Reenskaug于1978年在施乐帕克研究中心(Xerox PARC)提出,目的是为Smalltalk编程语言设计用户界面。其核心思想是将应用程序的逻辑分为三个独立组件:Model:处理数据逻辑和业务规则View:负责数据展示和用户界面Controller:接收用户输入并协调Model与View的交互Spring MVC与MVC的关系:Spring MVC是MVC模式在Spring框架中的具体化,同时扩展了传统MVC的功能以适应现代Web开发需求3.2 必需工具Postman:主要用于 API 的开发和测试。它提供了一个用户友好的界面,支持发送HTTP请求、管理请求历史、自动化测试以及团队协作下载地址:Download PostmanFiddler:是一个网络调试代理工具,主要用于监控和分析HTTP/HTTPS流量。它可以捕获设备与服务器之间的所有请求和响应,支持修改请求、重放请求以及性能分析下载地址:Fiddler - Download3.3 RequestMapping作用:是Spring MVC中最核心、最基础的注解之一,用于将HTTP请求映射到具体的方法上注解级别:类+方法作为类注解:可以为整个类提供一个统一的url前缀(可有可无)作为方法注解:指定该方法负责处理哪个url的请求(强制要求)@RequestMapping("/HelloController")//@RestController声明该类是一个Spring MVC控制器@RestControllerpublic class HelloController { //0.不接收参数 //作为 方法注解 时,@RequestMapping(value = "/hello",method = RequestMethod.GET)等同于@GetMapping(value = "/hello") //设置返回的响应是json格式,produces = "application/json" @RequestMapping(value = "/hello",method = RequestMethod.GET,produces = "application/json") public String hello() { return "{\"Hello\" : World}"; } //1.一个参数 @RequestMapping("/receiveAge1") //不传参或者传递的参数名不匹配时默认为null public String receiveAge1(Integer age) { return "接收到参数 age:" + age; } @RequestMapping("/receiveAge2") //不传参或者传递的参数名不匹配时尝试设置为null,但int无法被设置为null,所以抛出IllegalStateException public String receiveAge2(int age) { return "接收到参数 age:" + age; } //2.接收数组 @RequestMapping("/receiveArray") public String receiveArray(String[] array) { return "接收到参数 array:" + Arrays.toString(array); } //3.接收对象,需要保证传递的参数名称和数量与Java对象保持一致 @RequestMapping("/receivePerson") public String receivePerson(Person person) { return "接收到参数 person:" + person; }}3.4 RequestBody作用:将HTTP请求体中的json数据绑定到Java对象(方法注解)注解级别:方法 @RequestMapping("/receivePerson") //@RequestBody接收JSON格式的数据 public String receivePerson(@RequestBody Person person) { return "接收到参数 person:" + person; }3.5 RequestParam作用:是Spring MVC框架中从HTTP请求中提取参数/查询字符串的注解,主要用于将请求参数绑定到控制器方法的参数上注解级别:方法 @RequestMapping("/receiveRename") //@RequestParam将url中key=name的查询字符串绑定到控制器的userName参数上 //required = false设置该参数为非必传(默认为true,必传) public String receiveRename(@RequestParam(value = "name",required = false) String userName) { return "接收到参数name:" + userName; }注意:需要接收多个同名参数时(如param=value1¶m=value2),直接绑定到List类型需通过该注解明确声明 @RequestMapping("/receiveList1") public String receiveList1(ArrayList<String> list) { //返回的list为空 return "接收到参数 list:" + list;(1)在Spring MVC中,参数绑定机制对集合类型和数组类型的处理存在差异(2)使用ArrayList< String >作为方法参数时,必须显式添加@RequestParam注解,原因如下:默认绑定规则:Spring默认将单个请求参数的值绑定到简单类型(如 String、int)或单个对象。对于集合类型,框架无法自动推断是否需要将多个同名参数合并为集合需要明确指示:@RequestParam注解会告知Spring将同名请求参数的值收集到一个集合中(3)数组(如 String[])无需 @RequestParam 注解即可正确接收,原因如下:内置支持:Spring对数组类型有原生支持,能自动将多个同名请求参数值绑定到数组。这是框架的默认行为,无需额外配置 @RequestMapping("/receiveList2") public String receiveList2(@RequestParam(required = false) ArrayList<String> list) { //正确返回 return "接收到参数 list:" + list; } @RequestMapping("/receiveList3") public String receiveList3(List<String> list) { //报错 return "接收到参数 list:" + list; }后端报错:java.lang.IllegalStateException: No primary or single unique constructor found for interface java.util.List。receiveList3方法使用List< String >接口类型而非具体实现类。Spring虽然支持接口类型参数绑定,但需要满足特定条件:必须配合@RequestParam注解使用不能直接使用未注解的接口类型参数报错根本原因:Spring尝试实例化List接口失败(接口不可实例化)3.6 PathVariable作用:用于从URL路径中提取变量值并绑定到方法的参数上注解级别:方法 @RequestMapping("/receivePath/{article}/{blog}") //required = false设置该参数为非必传(默认为true,必传) public String receivePath(@PathVariable(value = "article",required = false)Integer title,@PathVariable(value = "blog",required = false)String content) { return "接收到参数 article:" + title + " blog:" + content; }3.7 RequestPart作用:用于处理 HTTP 请求中的 multipart/form-data 类型数据,通常用于文件上传或同时上传文件和其他表单字段的场景注解级别:方法 @RequestMapping("/receiveFile") public String receiveFile(@RequestPart(value = "file",required = false) MultipartFile imgFile,@RequestParam(value = "userName",required = false) String name) { //返回原始的文件名 return "用户:" + name+ ",接收到文件:" +imgFile.getOriginalFilename(); }3.8 Controller&ResponseBody&RestControllerController作用:是Spring MVC中的核心注解,用于标记一个类作为Web请求的处理器(声明一个类是一个Spring MVC控制器),负责处理HTTP请求并返回视图注解级别:类ResponseBody作用:指示方法返回值应直接写入HTTP响应体,而非通过视图解析器渲染注解级别:类+方法@RequestMapping("/ControllerResponse")@Controllerpublic class ControllerResponse { //返回视图 @RequestMapping("/HTMLView") public String HTMLView(){ return "/show.html"; } //返回数据 @ResponseBody @RequestMapping("/HTMLData") public String HTMLData(){ return "/show.html"; }}RestController作用:是Spring MVC中的一个组合注解,它结合了@Controller和@ResponseBody的功能,标记的类所有方法返回值默认直接作为 HTTP 响应体(JSON/XML 等格式),无需额外视图渲染注解级别:类4.GiteeGitee地址:九转苍翎本文源码:SpringBoot_Demo1———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2401_89167985/article/details/148194312
-
前言 自古以来,中秋佳节便与圆月紧密相连,成为人们寄托思念与团圆之情的象征。在民间流传着这样一种说法:“十五的月亮十六圆”,仿佛这已成为一种铁律,深入人心。然而,这种说法是否真的站得住脚呢?在这背后,隐藏着怎样的天文奥秘?又是否可以通过科学的方法来验证这一传统观念呢?在科技飞速发展的今天,我们不妨借助编程的力量,运用Java语言来实证求解,揭开中秋满月的真相。 中秋赏月的传统由来已久,早在《周礼》中就有“中秋夜迎寒”的记载,而到了唐代,中秋赏月、玩月的风俗开始盛行。文人墨客们更是留下了许多描写中秋月夜的佳作,如苏轼的“但愿人长久,千里共婵娟”,将中秋的月与人间的思念紧密相连,赋予了中秋月深厚的文化内涵。在这样的文化背景下,“十五的月亮十六圆”这一说法也逐渐流传开来,成为人们茶余饭后的话题之一。然而,这种说法真的准确无误吗? 本文通过Java实证求解中秋满月的时间,不仅可以验证传统的说法,还可以更深入地了解天文学中的相关知识。这不仅是一次对传统观念的挑战,也是一次对科学方法的实践。无论最终的结果如何,这一过程都将让我们对中秋满月有更深刻的认识,也将让我们感受到科学的魅力和力量。在接下来的章节中,我们将详细介绍如何使用Java语言进行天文数据的处理和计算,以及如何通过模拟实验来验证“十五的月亮十六圆”这一说法。我们将逐步展开这一探索之旅,最终揭示中秋满月的真相。让我们一起踏上这段充满趣味和挑战的旅程,用科学的视角重新审视中秋的圆月,探索其中隐藏的奥秘。一、天文上的满月 在天文学中,月亮的圆缺变化是一个非常有趣且复杂的自然现象,这种变化主要源于月球绕地球的公转运动。月球绕地球运行一周的时间大约是29.5天,这个周期被称为一个“朔望月”。在这个周期中,月球相对于太阳的位置不断变化,从而导致我们从地球上看到的月相也随之改变。博主不是专业天文专业,这里仅分享一些简单的满月基础知识,让大家有一个概念。1、形成原理及定义 说到满月就必须提及月相,月相的形成是由于太阳光照射月球的不同部分,而我们从地球上看到的只是月球被太阳照亮的那一部分。随着月球绕地球的公转,被太阳照亮的部分逐渐增加,依次出现“娥眉月”“上弦月”“凸月”“满月”“下弦月”“残月”等不同的月相。其中满月是指月球完全被太阳照亮的那一面朝向地球,此时月球与太阳在地球的两侧,三者几乎在一条直线上。理论上,满月应该出现在农历的十五或十六,但实际的情况并非总是如此。由于月球的公转轨道是椭圆形的,且受到多种因素的影响,如地球的引力、太阳的引力等,月球的实际运行轨迹并非完全规律,因此满月出现的时间也会有所变化。2、出现时间及观测 “十五的月亮十六圆”这一说法广为流传,但实际上满月并不总是出现在农历的十六。根据天文观测数据,满月可能出现在农历的十四到十七之间的任何一天。例如,在某些年份,满月可能出现在农历十四的晚上,而在另一些年份,满月可能出现在农历十七的早晨。这种变化是由于月球的公转速度和轨道形状的不规则性所导致的。满月是观测月球的最佳时机之一,因为此时月球的整个盘面都被照亮,可以清晰地看到月球表面的山脉、陨石坑和月海等特征。在满月期间,月球的亮度会达到最大,这使得它在夜空中格外明亮。3、文化意义 在许多文化中,满月都具有重要的象征意义。在中国文化中,满月象征着团圆和完满,因此中秋节成为了家人团聚的重要节日。在西方文化中,满月也常常与神秘和浪漫联系在一起,许多文学作品和民间传说都以满月为背景。二、Java模拟月满计算 随着计算机技术的发展,我们有了更强大的工具来探索和验证这些天文现象。Java作为一种广泛使用的编程语言,具有强大的功能和灵活性,可以用来编写各种复杂的算法和程序。在本研究中,我们将利用Java语言编写程序,通过计算月球在不同时间的位置,来确定中秋满月的具体时间。我们将收集多年来的天文数据,包括月球的公转周期、轨道参数等,然后利用这些数据进行模拟计算。通过这种方式,我们可以得到一个较为准确的中秋满月时间表,从而验证“十五的月亮十六圆”这一说法的准确性。1、整体实现逻辑 使用Java求解中秋满月整体时间逻辑如下:public class MidAutumnFullMoonCalculator { // 主计算方法 public static Date calculateFullMoonTime(int year, int month, int day) { ... } // 核心天文算法 private static double calculateFullMoonJulianDay(double jd) { ... } // 辅助方法 private static double normalizeAngle(double angle) { ... } private static double calendarToJulianDay(Calendar cal) { ... } private static Calendar julianDayToCalendar(double jd) { ... }}AI写代码java运行2、主计算方法详解 功能:这是程序的入口点,接收农历中秋的公历日期,返回精确的满月时刻,核心方法如下:/** * -计算指定农历中秋日期的月亮最圆时刻 * @param year 年份 * @param month 农历月份(八月) * @param day 农历日期(十五) * @return 月亮最圆时刻的Date对象 */public static Date calculateFullMoonTime(int year, int month, int day) { // 创建农历中秋日期(使用中午12点作为基准时间) Calendar midAutumnDate = Calendar.getInstance(); midAutumnDate.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); midAutumnDate.set(year, month - 1, day, 12, 0, 0); // month-1因为Calendar月份从0开始 // 计算精确的满月时刻 return calculatePreciseFullMoonTime(midAutumnDate);}AI写代码java运行 参数说明:year:公历年份(如2024)month:公历月份(如9)day:公历日期(如17) 处理流程:创建Calendar对象,设置为北京时间将时间设为中午12点作为计算基准调用核心算法计算精确的满月时刻3、核心天文算法详解 核心算法是相关计算中最核心的内容,主要包括儒略日的计算、时间参数的计算、天文参数计算和周期项修正等内容,这里的天文计算采用近似计算,如需精度计算,请使用更精准的天文算法。3.1 儒略日计算基础/** * -计算满月时刻的儒略日 * -基于Jean Meeus的天文算法 */private static double calculateFullMoonJulianDay(double jd) { // 计算从2000年1月6日(基准新月)开始的月相周期数 double k = Math.floor((jd - 2451550.09765) / 29.530588853); // 满月对应的k值(新月+0.5) k = k + 0.5; // 计算T(儒略世纪数) double T = k / 1236.85; //----其它计算}AI写代码java运行 儒略日(Julian Day):天文学中常用的连续时间计数法,从公元前4713年1月1日格林尼治平午开始计算。月相周期数k:2451550.09765:2000年1月6日18:14的儒略日,作为一个基准新月时刻29.530588853:一个朔望月的平均长度(天)k:从基准时间开始经过的月相周期数k + 0.5:从新月到满月是半个周期3.2 时间参数计算// 计算T(儒略世纪数)double T = k / 1236.85; // 计算基础儒略日double JDE = 2451550.09765 + 29.530588853 * k + 0.0001337 * T * T - 0.000000150 * T * T * T + 0.00000000073 * T * T * T * T;AI写代码java运行 T(儒略世纪数):以36525天为一世纪的时间单位,用于高阶项的计算。 (儒略历书日):考虑了长期项修正的基础满月时刻。3.3 天文参数计算 // 计算太阳平近点角 double M = normalizeAngle(2.5534 + 29.10535669 * k - 0.0000218 * T * T - 0.00000011 * T * T * T); // 计算月亮平近点角double Mprime = normalizeAngle(201.5643 + 385.81693528 * k + 0.1017438 * T * T + 0.00001239 * T * T * T - 0.000000058 * T * T * T * T); // 计算月亮升交点平黄经double F = normalizeAngle(160.7108 + 390.67050274 * k - 0.0016341 * T * T - 0.00000227 * T * T * T + 0.000000011 * T * T * T * T); // 计算Omega(月亮轨道升交点经度)double Omega = normalizeAngle(124.7746 - 1.56375580 * k + 0.0020691 * T * T + 0.00000215 * T * T * T);AI写代码java运行 天文参数说明:M(太阳平近点角):太阳在轨道上的平均位置角度系数:2.5534° + 29.10535669°/周期反映地球公转轨道的椭圆性影响M'(月亮平近点角):月亮在轨道上的平均位置角度系数:201.5643° + 385.81693528°/周期反映月球公转轨道的椭圆性影响F(月亮升交点平黄经):月球轨道与黄道交点的平均位置系数:160.7108° + 390.67050274°/周期反映月球轨道平面的进动Ω(月亮轨道升交点经度):更精确的轨道交点位置系数:124.7746° - 1.56375580°/周期3.4 周期项修正计算// 转换为弧度double M_rad = Math.toRadians(M);double Mprime_rad = Math.toRadians(Mprime);double F_rad = Math.toRadians(F);double Omega_rad = Math.toRadians(Omega);// 计算周期项修正double correction = 0;// 主要修正项correction += -0.40720 * Math.sin(Mprime_rad);correction += 0.17241 * 0.016708617 * Math.sin(M_rad);correction += 0.01608 * Math.sin(2 * Mprime_rad);correction += 0.01039 * Math.sin(2 * F_rad);correction += 0.00739 * 0.016708617 * Math.sin(Mprime_rad - M_rad);correction += -0.00514 * 0.016708617 * Math.sin(Mprime_rad + M_rad);correction += 0.00208 * 0.016708617 * 0.016708617 * Math.sin(2 * M_rad);correction += -0.00111 * Math.sin(Mprime_rad - 2 * F_rad);correction += -0.00057 * Math.sin(Mprime_rad + 2 * F_rad);correction += 0.00056 * 0.016708617 * Math.sin(2 * Mprime_rad + M_rad);correction += -0.00042 * Math.sin(3 * Mprime_rad);correction += 0.00042 * 0.016708617 * Math.sin(M_rad + 2 * F_rad);correction += 0.00038 * 0.016708617 * Math.sin(M_rad - 2 * F_rad);correction += -0.00024 * 0.016708617 * Math.sin(2 * Mprime_rad - M_rad);correction += -0.00017 * Math.sin(Omega_rad);correction += -0.00007 * Math.sin(Mprime_rad + 2 * M_rad);correction += 0.00004 * Math.sin(2 * Mprime_rad - 2 * F_rad);correction += 0.00004 * Math.sin(3 * M_rad);correction += 0.00003 * Math.sin(Mprime_rad + M_rad - 2 * F_rad);correction += 0.00003 * Math.sin(2 * Mprime_rad + 2 * F_rad);correction += -0.00003 * Math.sin(Mprime_rad + M_rad + 2 * F_rad);correction += 0.00003 * Math.sin(Mprime_rad - M_rad + 2 * F_rad);correction += -0.00002 * Math.sin(Mprime_rad - M_rad - 2 * F_rad);correction += -0.00002 * Math.sin(3 * Mprime_rad + M_rad);correction += 0.00002 * Math.sin(4 * Mprime_rad); // 应用修正double preciseJDE = JDE + correction;AI写代码java运行修正项原理:每个修正项都对应一个特定的天文效应:-0.40720 × sin(M'):月球椭圆轨道的主要修正(中心差)0.17241 × e × sin(M):地球轨道偏心率对月相的影响0.01608 × sin(2M'):月球轨道的二阶椭圆项0.01039 × sin(2F):月球轨道倾角的影响0.00739 × e × sin(M' - M):地球和月球轨道相互影响-0.00514 × e × sin(M' + M):地球和月球轨道的组合效应e = 0.016708617:地球轨道偏心率这些修正项基于布朗月球运动理论,考虑了月球轨道的各种摄动因素。4、辅助方法详解 本小节将对辅助方法进行简单介绍。4.1 角度标准化/** * -将角度标准化到0-360度范围内 */private static double normalizeAngle(double angle) { angle = angle % 360; if (angle < 0) { angle += 360; } return angle;}AI写代码java运行 功能:将角度限制在0-360度范围内,避免数值溢出。4.2 日历与儒略日转换/** * -将Calendar转换为儒略日 */private static double calendarToJulianDay(Calendar cal) { int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH) + 1; int day = cal.get(Calendar.DAY_OF_MONTH); int hour = cal.get(Calendar.HOUR_OF_DAY); int minute = cal.get(Calendar.MINUTE); int second = cal.get(Calendar.SECOND); double decimalHour = hour + minute / 60.0 + second / 3600.0; if (month <= 2) { year--; month += 12; } int a = year / 100; int b = 2 - a + a / 4; return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + decimalHour / 24.0 + b - 1524.5;}AI写代码java运行 转换公式:标准的天文儒略日计算公式,考虑了:闰年规则格里高利历改革(1582年)时间的小数部分处理4.3 儒略日转日历/** * -将儒略日转换为Calendar */private static Calendar julianDayToCalendar(double jd) { jd += 0.5; double z = Math.floor(jd); double f = jd - z; double a; if (z < 2299161) { a = z; } else { double alpha = Math.floor((z - 1867216.25) / 36524.25); a = z + 1 + alpha - Math.floor(alpha / 4); } double b = a + 1524; double c = Math.floor((b - 122.1) / 365.25); double d = Math.floor(365.25 * c); double e = Math.floor((b - d) / 30.6001); double day = b - d - Math.floor(30.6001 * e) + f; int month = (int) (e < 14 ? e - 1 : e - 13); int year = (int) (month > 2 ? c - 4716 : c - 4715); double time = day - Math.floor(day); int hour = (int) (time * 24); int minute = (int) ((time * 24 - hour) * 60); int second = (int) Math.round((((time * 24 - hour) * 60 - minute) * 60)); // 处理秒数进位 if (second >= 60) { second = 0; minute++; } if (minute >= 60) { minute = 0; hour++; } Calendar cal = Calendar.getInstance(); cal.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); cal.set(year, month - 1, (int) Math.floor(day), hour, minute, second); cal.set(Calendar.MILLISECOND, 0); return cal;}AI写代码java运行 关键点:jd += 0.5:儒略日从中午开始,调整为从午夜开始处理格里高利历改革(1582年10月4日后跳过10天)精确的时间分量计算三、近年中秋满月计算及对比 本节将结合实例对每年的中秋月满时间进行计算,通过本小节就可以获取每年的满月日期和具体的时间,并且与官方提供的时间进行对比,大家通过对比就可以知晓问题的开始,是不是所有的月亮都是十六圆了。 1、近年中秋满月计算/** * -测试方法 - 计算未来几年的中秋节月亮最圆时刻 */public static void main(String[] args) { // 已知的农历中秋日期(公历日期) int[][] midAutumnDates = { {2019, 9, 13}, // 2019年中秋节 {2020, 10, 1}, // 2020年中秋节 {2021, 9, 21}, // 2021年中秋节 {2022, 9, 10}, // 2022年中秋节 {2023, 9, 29}, // 2023年中秋节 {2024, 9, 17}, // 2024年中秋节 {2025, 10, 6}, // 2025年中秋节 {2026, 9, 25}, // 2026年中秋节 }; SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒"); sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); System.out.println("中秋节月亮最圆时刻计算结果:"); System.out.println("================================="); for (int[] date : midAutumnDates) { int year = date[0]; int month = date[1]; int day = date[2]; Date fullMoonTime = calculateFullMoonTime(year, month, day); System.out.printf("%d年中秋节(公历%d月%d日)月亮最圆时刻: %s%n", year, month, day, sdf.format(fullMoonTime)); }}AI写代码java运行 接下来我们在IDE中运行意以上成就可以得到以下结果: 以上就是实现一个从2019年到2026年,跨度为7年的中秋满月计算过程。2、近年计算与公布时间对比 通过以上7年的计算,再结合官方公布的满月日期及时刻,来对比一下我们的计算方法与官方公布的时间相差是多少?年份 中秋(公历) 满月时间(本地) 是否当天 满月时间(官方公布) 误差2019 2019-9-13 09月14日 08时39分21秒 否(十六) 9月14日12时33分 3时54分2020 2020-10-1 10月02日 01时28分17秒 否(十六) 10月2日 05时5分 3时37分2021 2021-9-21 09月21日 03时58分38秒 是(十五) 9月21日 07时54分 3时56分2022 2022-9-10 09月10日 13时34分12秒 是(十五) 9月10日 17时59分 4时25分2023 2023-9-29 09月29日 13时49分05秒 是(十五) 9月29日 17时58分 4时8分2024 2024-9-17 09月18日 06时15分39秒 否(十六) 9月18日 10时34分 4时19分2025 2024-10-06 10月07日 07时41分10秒 否(十六) 10月7日 11时48分 4时7分 结合近七年的满月日期及时刻来看,并不是所有的中秋月圆都是十六圆,有的是当天就圆了。所以,从这个角度来定义,十五的月亮十六圆可不是准确的哦。通过这种本地近似的计算,虽然在具体的时刻上有一些误差,但是日期是与官方公布的是完全一致的,时刻的误差通过近7年的验证,相差时间在4个小时左右,所以未来可以结合更长序列的时间进行相应的修正。四、总结 以上就是本文的主要内容,本文通过Java实证求解中秋满月的时间,不仅可以验证传统的说法,还可以更深入地了解天文学中的相关知识。这不仅是一次对传统观念的挑战,也是一次对科学方法的实践。无论最终的结果如何,这一过程都将让我们对中秋满月有更深刻的认识,也将让我们感受到科学的魅力和力量。通过Java满月近似求解,并结合2019年到2025年的中秋满月日期时刻的计算,得出了重要的一个结论,十五的月亮不一定十六圆,通过严谨的程序计算得到的数据支撑。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/yelangkingwuzuhu/article/details/152717752
-
1.配置文件1.1 概述计算机配置文件:用于存储系统、应用程序的设置信息,通常以文本或结构化数据格式(如JSON、XML、INI等)保存。其核心功能包括但不限于:参数定制:允许用户或管理员调整软件或硬件的运行参数环境适配:根据不同设备或场景加载特定配置(如开发/生产环境)持久化存储:确保重启后设置仍生效SpringBoot配置文件:SpringBoot支持多种类型的配置文件,常见的格式包括properties、yaml和yml,主要用于集中管理应用程序的各种配置参数,简化部署和开发过程中的环境切换YAML和YML本质上是相同的文件格式,只是文件扩展名的不同,两者在功能和使用上没有区别1.2 propertiesproperties配置文件是最早期的配置⽂件格式,也是创建SpringBoot项⽬默认的配置⽂件采用常见的键值对格式(key=value)支持#开头的注释#应用程序名称spring.application.name=configuration#应用程序端口号server.port=8080#数据库连接信息spring.datasource.url=jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=falsespring.datasource.username=rootspring.datasource.password=root1.3 yml采用键值对格式(key: value),冒号后必须有空格数据序列化格式,通过缩进表示层级关系支持#开头的注释spring: application: #应用程序名称 name: configuration #数据库连接信息 datasource: url: jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=false username: root password: root#应用程序端口号server: port: 80801.4 优缺点对比properties优点:语法简单直观,采用key=value形式,适合初学者快速上手与Java生态兼容性极强缺点:缺乏层次结构,复杂配置时容易冗余。上述配置数据库连接信息时spring.datasource前缀冗余不支持数据类型定义,所有值均为字符串,需手动转换yml优点:层次化结构清晰,通过缩进表示层级,适合复杂配置场景支持数据类型(如布尔值、数字),减少手动类型转换缺点:格式错误易导致解析失败(容易忽略冒号后空格)部分旧版工具链兼容性较差,需额外依赖解析库注:SpringBoot同时支持两种格式,混合使用时若key重复,properties优先级高于yml1.5 @Value注解作用:是Spring框架提供了一个@Value注解(org.springframework.beans.factory.annotation.Value),用于将外部配置文件中的值注入到Spring管理的Bean中示例:(properties和yml的读取方式相同)package org.example.configuration.config;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Configuration;@Configurationpublic class Config { @Value("${spring.application.name}") private String applicationName; @Value("${server.port}") private Integer port; @Value("${spring.datasource.url}") private String url; @Value("${spring.datasource.username}") private String username; @Value("${spring.datasource.password}") private String password; public void print() { System.out.println("applicationName=" + applicationName); System.out.println("port=" + port); System.out.println("url=" + url); System.out.println("username=" + username); System.out.println("password=" + password); }}package org.example.configuration;import org.example.configuration.config.Config;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ApplicationContext;@SpringBootApplicationpublic class ConfigurationApplication { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(ConfigurationApplication.class, args); Config config = context.getBean(Config.class); config.print(); }}运行结果:applicationName=configurationport=8080url=jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=falseusername=rootpassword=root2.mybatis2.1 概述MyBatis是一款优秀的持久层框架,支持自定义 SQL、存储过程、高级映射以及多种配置方式。它消除了几乎所有的JDBC代码和参数的手动设置以及结果集的检索支持存储过程:指的是数据库管理系统(DBMS)允许用户创建、存储和执行存储过程的能力。存储过程是一组预编译的SQL语句,存储在数据库中,可以被应用程序调用执行支持高级映射:指通过配置或注解实现复杂SQL查询结果与Java对象之间的灵活转换。其核心目标是简化数据库关联操作,提升开发效率支持多种配置方式:mybatis支持注解和xml两种配置方式2.2 前置操作引入依赖:Spring Web,Mybatis Framework,MySQL Driver,Lombok在application.properties/yml中添加数据库连接信息:#应用程序名称spring.application.name=configuration#应用程序端口号server.port=8080#数据库连接信息spring.datasource.url=jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=falsespring.datasource.username=rootspring.datasource.password=root#自动驼峰转换mybatis.configuration.map-underscore-to-camel-case=truespring: application: #应用程序名称 name: configuration #数据库连接信息 datasource: url: jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=false username: root password: root#应用程序端口号server: port: 8080mybatis: configuration: map-underscore-to-camel-case: true #自动驼峰转换SQL命名规范:采用下划线分隔单词(如order_detail)Java命名规范:大驼峰/小驼峰2.3 注解2.3.1 配置1.创建一个接口,并使用 @Mapper注解 修饰import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface BlogMapper { //其他代码}@Mapper注解:允许开发者直接在接口方法上通过注解配置SQL语句,无需编写XML映射文件。适用于简单SQL场景,能显著减少配置量2.初始化数据create table blog (id int primary key auto_increment,name varchar(128),age int);insert into blog values (null,'刘备',30),(null,'关羽',28),(null,'张飞',25);3.创建对应实体类import lombok.Data;@Datapublic class PersonInfo { private Integer id; private String name; private Integer age; public PersonInfo(Integer id, String name, Integer age) { this.id = id; this.name = name; this.age = age; } public PersonInfo() { }}2.3.2 CRUDimport com.example.spring_mybatis.model.PersonInfo;import org.apache.ibatis.annotations.*;@Mapperpublic interface BlogMapper { @Select("select * from blog") List<PersonInfo> getPersonInfoAll(); @Insert("insert into blog values (#{id},#{name},#{age})") Integer addPerson(PersonInfo person); @Update("update blog set name = #{name},age = #{age} where id = #{id}") Integer updatePerson(PersonInfo personInfo); @Delete("delete from blog where id = #{id}") Integer deletePerson(Integer id);}按住alt+insert,可在test目录下生成以上方法的测试方法import com.example.spring_mybatis.model.PersonInfo;import lombok.extern.slf4j.Slf4j;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest@Slf4jclass BlogMapperTest { private final BlogMapper blogMapper; @Autowired public BlogMapperTest(BlogMapper blogMapper) { this.blogMapper = blogMapper; } @Test void getPersonInfoAll() { List<PersonInfo> personInfoAll = blogMapper.getPersonInfoAll(); log.info("查询成功,personInfoAll:{}",personInfoAll.toString()); //查询成功,personInfoAll:[PersonInfo(id=1, name=刘备, age=30), //PersonInfo(id=2, name=关羽, age=28), //PersonInfo(id=3, name=张飞, age=25)] } @Test void addPerson() { Integer ret = blogMapper.addPerson(new PersonInfo(null, "赵云", 25)); log.info("添加成功,影响行数:{}",ret.toString());//添加成功,影响行数:1 } @Test void updatePerson() { Integer ret = blogMapper.updatePerson(new PersonInfo(1, "刘备", 35)); log.info("更新成功,影响行数:{}",ret.toString());//更新成功,影响行数:1 } @Test void deletePerson() { Integer ret = blogMapper.deletePerson(4); log.info("删除成功,影响行数:{}",ret.toString());//删除成功,影响行数:1 }}2.3.3 @Param作用:用于在Mapper接口方法中为形式参数指定名称。当方法有多个参数时,通过该注解明确SQL中引用的参数名,避免依赖参数顺序@Mapperpublic interface BlogMapper { //@Param @Update("update blog set name = #{name},age = #{age} where id = #{id}") Integer updatePersonInfo(@Param("id") Integer userId,@Param("name") String userName,@Param("age") Integer userAge);}AI写代码java运行123456@SpringBootTest@Slf4jclass BlogMapperTest { private final BlogMapper blogMapper; @Autowired public BlogMapperTest(BlogMapper blogMapper) { this.blogMapper = blogMapper; } @Test void updatePersonInfo() { Integer ret = blogMapper.updatePersonInfo(1, "刘玄德", 30); log.info("更新成功,影响行数:{}",ret.toString());//更新成功,影响行数:1 }}2.4 xml2.4.1 配置1.创建一个接口,并使用 @Mapper注解 修饰import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface BlogXMLMapper { //其他代码}2.配置mybatis的xml文件路径mybatis: mapper-locations: classpath:mybatis/**Mapper.xml #配置mybatis的xml文件路径 #标识位于resources/mybatis路径下任何以Mapper结尾的xml文件为mybatis的配置文件3.在resources/mybatis路径下创建以Mapper结尾的xml文件,并添加如下代码<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--namespace:用于指定该XML文件对应的Java接口或类的全限定名--><mapper namespace="com.example.spring_mybatis.mapper_blog.BlogXMLMapper"></mapper>2.4.2 示例在接口中声明方法import com.example.spring_mybatis.model_blog.PersonInfo;import org.apache.ibatis.annotations.Mapper;import java.util.List;@Mapperpublic interface BlogXMLMapper { List<PersonInfo> getPersonInfoAll();}在对应xml文件中实现接口方法id:是MyBatis映射文件中SQL语句的唯一标识符。需与Mapper接口中的方法名一致,保证映射正确resultType:指定SQL查询结果映射的Java对象类型,需为全限定类名<mapper namespace="com.example.spring_mybatis.mapper_blog.BlogXMLMapper"> <select id="getPersonInfoAll" resultType="com.example.spring_mybatis.model_blog.PersonInfo"> select * from blog </select></mapper>按住alt+insert,可在test目录下生成以上方法的测试方法import com.example.spring_mybatis.model_blog.PersonInfo;import lombok.extern.slf4j.Slf4j;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest@Slf4jclass BlogXMLMapperTest { private final BlogXMLMapper blogXMLMapper; @Autowired public BlogXMLMapperTest(BlogXMLMapper blogXMLMapper) { this.blogXMLMapper = blogXMLMapper; } @Test void getPersonInfoAll() { List<PersonInfo> personInfoAll = blogXMLMapper.getPersonInfoAll(); log.info("查询成功,personInfoAll:{}",personInfoAll.toString()); }}运行结果:2.5 动态SQL动态SQL:指在程序运行时根据条件或参数动态生成的SQL语句。与静态SQL相比,动态SQL更具灵活性,适用于需要根据不同条件构建查询的场景。例如,在某些web/app进行账号注册时会出现非必填选项mybatis的注解和xml两种方式都能实现动态SQL,但xml较为方便,所以下文使用xml来实现动态SQL2.5.1 trim标签作用:用于自定义字符串截取规则。包含四个属性:prefix:最终结果添加前缀suffix:最终结果添加后缀prefixOverrides:去除首部指定内容suffixOverrides:去除尾部指定内容2.5.2 if标签作用:用于条件判断,通常在where或set语句中使用。当test表达式的值为true时,包含标签内的SQL片段 <insert id="addPersonInfo"> insert into blog <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null"> id, </if> <if test="name != null"> name, </if> <if test="age != null"> age, </if> </trim> values <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null"> #{id}, </if> <if test="name != null"> #{name}, </if> <if test="age != null"> #{age}, </if> </trim> </insert>2.5.3 where标签作用:替代SQL中的where关键字。当if条件成立时才会加入SQL片段,并自动去除第一个子句的and/or <select id="getPersonInfoByNameAndAge"> select * from blog <where> <if test="name != null"> and name = #{name} </if> <if test="age != null"> and age = #{age} </if> </where> </select>2.5.4 set标签作用:用于update语句。当if条件成立时才会加入SQL片段,并自动去除最后一个子句的逗号、 <update id="updatePersonInfo"> update blog <set> <if test="name != null"> name = #{name}, </if> <if test="age != null"> age = #{age}, </if> </set> <where> and id = #{id} </where> </update>2.5.5 foreach标签作用:用于集合遍历。主要属性:collection:集合参数名item:当前元素变量名open/close:包围符号separator:分隔符 @Test void getPersonInfoById() { ArrayList<Integer> ids = new ArrayList<>(); ids.add(1); ids.add(2); ids.add(3); List<PersonInfo> personInfoById = blogXMLMapper.getPersonInfoById(ids); System.out.println(personInfoById); } <select id="getPersonInfoById" resultType="com.example.spring_mybatis.model_blog.PersonInfo"> select * from blog where id in <foreach collection="ids" item="id" open="(" close=")" separator=","> #{id} </foreach> </select>2.5.6 include标签作用:用于引用SQL片段,通过refid指定要引用的片段id。需配合sql标签使用,实现代码复用 <sql id="collection"> id,name,age </sql> <select id="getPersonInfoAll" resultType="com.example.spring_mybatis.model_blog.PersonInfo"> select <include refid="collection"> </include> from blog </select>2.6 主键返回主键返回:指在数据库插入操作后,自动获取刚插入记录的主键值。在mybatis中使用注解和xml都能获取到返回的主键1.注解实现@Mapperpublic interface BlogMapper { @Options(useGeneratedKeys = true,keyProperty = "id") @Insert("insert into blog values (#{id},#{name},#{age})") Integer addPerson(PersonInfo person);}@SpringBootTest@Slf4jclass BlogMapperTest { private final BlogMapper blogMapper; @Autowired public BlogMapperTest(BlogMapper blogMapper) { this.blogMapper = blogMapper; } @Test void addPerson() { PersonInfo personInfo = new PersonInfo(null, "黄忠", 60); Integer ret = blogMapper.addPerson(personInfo); log.info("添加成功,影响行数:{},返回的主键值:{}",ret.toString(),personInfo.getId());//添加成功,影响行数:1,返回的主键值:6 }}2.通过xml实现@SpringBootTest@Slf4jclass BlogXMLMapperTest { private final BlogXMLMapper blogXMLMapper; @Autowired public BlogXMLMapperTest(BlogXMLMapper blogXMLMapper) { this.blogXMLMapper = blogXMLMapper; } @Test void addPersonInfo() { PersonInfo personInfo = new PersonInfo(); personInfo.setAge(40); personInfo.setName("曹操"); Integer ret = blogXMLMapper.addPersonInfo(personInfo); log.info("添加成功,影响行数:{},返回的主键值:{}",ret.toString(),personInfo.getId());//添加成功,影响行数:1,返回的主键值:7 }} <insert id="addPersonInfo" useGeneratedKeys="true" keyProperty="id"> insert into blog <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null"> id, </if> <if test="name != null"> name, </if> <if test="age != null"> age, </if> </trim> values <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != null"> #{id}, </if> <if test="name != null"> #{name}, </if> <if test="age != null"> #{age}, </if> </trim> </insert>2.7 预编译/即时SQL预编译SQL(Prepared Statements):SQL语句在程序运行前被预先编译并存储在数据库中。执行时只需传递参数,无需重新编译SQL语句安全性高:通过参数化查询避免SQL注入攻击。参数化查询是一种将SQL语句与用户输入数据分离的数据库操作方式,查询语句中使用占位符(如?、@param等)代替直接拼接用户输入,执行时通过预编译机制将参数动态绑定到占位符位置性能优化:编译一次,多次执行,减少数据库开销即时SQL(Dynamic SQL):在程序运行时动态生成并立即编译执行,每次执行都可能涉及完整的SQL解析和编译过程灵活性高:可根据运行时条件动态拼接SQL语句潜在风险:直接拼接用户输入可能导致SQL注入性能开销:每次执行需重新编译#占位符会使用预编译机制,将参数值安全地绑定到SQL语句中,防止SQL注入攻击。MyBatis会将#替换为?,然后通过JDBC的预编译功能设置参数值$占位符直接进行字符串替换,将参数值拼接到SQL语句中,不会进行预编译或转义处理SQL注入攻击:当恶意输入" 'or 1 = '1 "时1.预编译SQL@Select("select * from blog where name= #{name}")List<UserInfo> queryByName(String name)预编译SQL会根据参数的类型判断是否需要加引号,上述name参数是String类型,需要加引号,这就是参数化查询的作用。最终SQL:select * from blog where name = " 'or 1='1 "; # 整个 'or 1='1 会作为name的值2.即时SQL@Select("select * from blog where name= '${name}'")List<UserInfo> queryByName(String name)即时SQL不会判断参数类型从而是否添加引号,所以需要手动加上单引号。最终SQL:select * from blog where name = ''or 1 = '1'; # 因为1=1是恒等式,所以该表的数据会被全部查———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2401_89167985/article/details/152804543
-
在 Java 中,int 和 Integer 是处理整数时最常用的两种类型,但二者在本质、用途、特性上有根本区别,核心差异可从以下维度清晰区分:一、本质区别:基本数据类型 vs 引用数据类型类型归属类别存储方式核心特点int基本数据类型直接存储数值(栈内存)不包含额外方法,仅存原始值Integer引用数据类型(类)存储对象引用(堆内存存对象,栈内存存地址)是 int 的包装类,包含工具方法简单理解:int 是 “裸数值”,Integer 是 “装着数值的盒子”—— 盒子里除了数值,还附带了操作数值的工具。二、核心差异对比1. 默认值不同int 作为基本类型,默认值为 0(如类中定义 private int num;,未赋值时 num 自动为 0);Integer 作为引用类型,默认值为 null(如 private Integer num;,未赋值时 num 为 null)。 ⚠️ 注意:若直接调用 Integer 变量的方法(如 num.toString())且未赋值,会触发 NullPointerException,而 int 不会。2. 适用场景不同int:适用于简单的数值计算、存储基本整数(如循环变量、年龄、分数等),性能更优(无需创建对象,节省内存);Integer:适用于需要对象特性的场景,例如:集合框架(List<Integer>、Map<String, Integer> 等,集合仅支持引用类型);泛型(<T> 类型参数必须是引用类型,不能是 int);需使用工具方法时(如数值转字符串 Integer.toString(num)、字符串转整数 Integer.parseInt("123"))。3. 特性差异:缓存机制Integer 有缓存池机制(JDK 1.5+),默认缓存 -128 ~ 127 范围内的整数对象,重复使用该范围的 Integer 时,不会创建新对象;而 int 无缓存,每次使用都是独立的数值。示例验证:java 运行 // Integer 缓存机制Integer a = 127; // 从缓存池获取对象Integer b = 127; // 复用同一缓存对象System.out.println(a == b); // true(引用地址相同)Integer c = 128; // 超出缓存范围,创建新对象Integer d = 128; // 新对象System.out.println(c == d); // false(引用地址不同)// int 无缓存,直接比较数值int e = 128;int f = 128;System.out.println(e == f); // true(数值相同) 4. 比较方式不同int 比较:用 == 直接比较数值(正确,因为存储的是原始值);Integer 比较:== 比较的是对象引用地址(仅在缓存范围内的整数会相等,超出范围则不等,易出错);正确比较方式:用 equals() 方法(比较数值),或先拆箱为 int 再用 ==。示例:java 运行 Integer x = 100;Integer y = new Integer(100); // 强制创建新对象(不走缓存)System.out.println(x == y); // false(引用地址不同)System.out.println(x.equals(y)); // true(数值相同)System.out.println((int)x == y); // true(拆箱后比较数值) 三、自动拆箱与装箱(JDK 1.5+)为简化 int 和 Integer 的转换,Java 提供自动拆箱(Unboxing) 和 自动装箱(Boxing):自动装箱:int 自动转为 Integer(如 Integer num = 10;,等价于 Integer num = Integer.valueOf(10););自动拆箱:Integer 自动转为 int(如 int val = num;,等价于 int val = num.intValue();)。注意:频繁拆装箱可能影响性能(如循环中大量转换),需避免不必要的操作。四、总结:如何选择?若仅需存储 / 计算基本整数,优先用 int(性能好、无空指针风险);若需集合、泛型、工具方法,或可能存储 null(如数据库字段允许为空),用 Integer;Integer 比较时务必用 equals(),避免用 == 踩缓存范围的坑。
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签