• [技术干货] Java 后端开发技术学习总结:实用代码示例与项目实操经验大公开
    一、引言在当今数字化时代,Java 后端开发技术占据着举足轻重的地位。从大型企业级应用到小型创业项目,Java 凭借其卓越的性能、强大的生态系统和跨平台特性,成为后端开发的首选语言之一。作为一名在 Java 后端领域摸爬滚打许久的开发者,我深知学习过程中的酸甜苦辣,也积累了不少实用的知识与经验。在这篇博客中,我将毫无保留地与大家分享我的学习总结,涵盖基础语法、常用框架、数据库交互以及项目实战中的宝贵实操经验,同时附上大量实用代码示例,希望能助力各位在 Java 后端开发的道路上少走弯路。二、Java 基础语法夯实数据类型与变量Java 是强类型语言,数据类型分为基本数据类型(如 int、double、char、boolean 等)和引用数据类型(如类、接口、数组)。在定义变量时,务必明确其数据类型,例如:int num = 10;double pi = 3.14;基本数据类型有其默认值,例如 int 默认值为 0,引用数据类型默认值为 null。在变量作用域方面,要注意块级作用域的影响,避免变量重复定义或在不该使用的地方使用。控制结构条件判断语句 if - else 是最常用的结构之一,例如:int score = 85;if (score >= 90) {    System.out.println("优秀");} else if (score >= 80 && score < 90) {    System.out.println("良好");} else {    System.out.println("再接再厉");}循环结构 for、while 和 do - while 各有适用场景。for 循环常用于已知循环次数的情况,如遍历数组:int[] arr = {1, 2, 3, 4, 5};for (int i = 0; i < arr.length; i++) {    System.out.println(arr[i]);}while 循环则适用于条件判断在前,只要条件满足就持续执行的场景,注意要避免死循环,确保循环条件能在某个时刻变为 false。数组与集合数组是固定长度的数据结构,定义时需指定长度:int[] numbers = new int[5];numbers[0] = 1;// 遍历数组for (int num : numbers) {    System.out.println(num);}集合框架(如 ArrayList、LinkedList、HashSet、HashMap 等)提供了更灵活的存储方式。ArrayList 适合频繁的随机访问,示例代码:ArrayList<String> list = new ArrayList<>();list.add("apple");list.add("banana");// 访问元素System.out.println(list.get(0));HashSet 用于去重,HashMap 用于键值对存储,合理选择集合类型能优化代码性能。三、常用 Java 后端框架探秘Spring 框架Spring 是 Java 后端开发的基石,其核心特性 IoC(控制反转)和 AOP(面向切面编程)极大地简化了开发流程。通过依赖注入,对象的创建和管理交给 Spring 容器,例如:@Servicepublic class UserService {    @Autowired    private UserRepository userRepository;    // 业务逻辑}在配置文件(如 application.properties 或 application.yml)中,可以设置数据库连接、端口等信息,Spring Boot 更是凭借其自动配置功能,让项目快速启动。Spring MVC用于构建 Web 应用,它将请求处理、模型、视图进行分离。定义一个简单的 Controller:@RestController@RequestMapping("/api")public class UserController {    @Autowired    private UserService userService;    @GetMapping("/users")    public List<User> getUsers() {        return userService.getAllUsers();    }}这里通过注解指定请求路径和方法类型,方便快捷地处理前端发来的 HTTP 请求,返回相应的数据。MyBatis专注于数据库访问层,它通过 XML 配置文件或注解的方式将 SQL 语句与 Java 方法绑定。在 XML 配置中:<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//XML" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.dao.UserDao">    <select id="getUserById" resultType="com.example.entity.User">        SELECT * FROM users WHERE id = #{id}    </select></mapper>对应的 Java 接口:@Mapperpublic interface UserDao {    User getUserById(int id);}这种方式使得 SQL 编写更加灵活,同时与 Java 代码解耦,方便维护。四、数据库交互实战JDBC(Java Database Connectivity)基础JDBC 是 Java 连接数据库的底层 API,首先需要加载驱动:Class.forName("com.mysql.cj.jdbc.Driver");然后建立连接:Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "password");执行 SQL 语句,例如查询:Statement statement = connection.createStatement();ResultSet resultSet = statement.executeQuery("SELECT * FROM users");while (resultSet.next()) {    System.out.println(resultSet.getString("name"));}最后记得关闭连接、语句和结果集,释放资源。使用连接池优化数据库连接频繁创建和关闭数据库连接开销较大,连接池(如 HikariCP、Druid 等)能有效解决这个问题。以 HikariCP 为例,配置连接池:HikariConfig hikariConfig = new HikariConfig();hikariConfig.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");hikariConfig.setUsername("root");hikariConfig.setPassword("password");HikariDataSource dataSource = new HikariDataSource(hikariConfig);从连接池中获取连接:Connection connection = dataSource.getConnection();连接池会自动管理连接的复用、空闲连接回收等,提升性能。数据库事务处理在涉及多个数据库操作且需保证原子性时,事务至关重要。使用 Spring 的事务管理,在 Service 层方法上添加注解:@Transactionalpublic void transferMoney(int fromUserId, int toUserId, double amount) {    // 从一个用户账户扣除金额    userRepository.decreaseBalance(fromUserId, amount);    // 向另一个用户账户增加金额    userRepository.increaseBalance(toUserId, amount);}若其中一个操作失败,整个事务将回滚,确保数据的一致性。五、项目实操经验大放送项目架构设计在启动一个项目前,合理的架构设计能避免后期的混乱。通常采用分层架构,如表现层(Controller)、业务逻辑层(Service)、数据访问层(Dao)、实体层(Entity)和工具层(Utils)。各层职责明确,相互协作,降低耦合度。考虑扩展性,例如设计接口时预留扩展字段,以便后续功能迭代时能轻松兼容新需求。日志管理引入日志框架(如 Log4j、Logback 等),在关键代码段添加日志记录:private static final Logger logger = LoggerFactory.getLogger(MyClass.class);public void doSomething() {    logger.info("开始执行 doSomething 方法");    // 业务逻辑    logger.info("doSomething 方法执行完毕");}通过配置日志级别,可以灵活控制日志输出的详细程度,方便在开发、测试和生产环境中排查问题。异常处理统一的异常处理机制能提升系统的稳定性。在 Spring 项目中,可以定义全局异常处理器:@ControllerAdvicepublic class GlobalExceptionHandler {    @ExceptionHandler(Exception.class)    public ResponseEntity<String> handleException(Exception e) {        logger.error("系统出现异常:", e);        return new ResponseEntity<>("系统繁忙,请稍后再试", HttpStatus.INTERNAL_SERVER_ERROR);    }}针对不同类型的异常(如业务异常、数据库异常等),可以提供个性化的错误提示,提升用户体验。六、总结与展望通过对 Java 后端开发技术从基础语法到框架应用,再到数据库交互和项目实战的深入学习,我们逐步构建起坚实的知识体系。然而,技术的发展日新月异,Java 生态也在不断演进。未来,我们需要持续关注新的框架版本、性能优化技巧以及云计算、微服务等新兴领域与 Java 后端的融合。希望这篇学习总结能成为各位读者在 Java 后端之旅中的得力伙伴,让我们一起在代码的世界里不断探索,创造出更强大、更高效的后端应用。————————————————原文链接:https://blog.csdn.net/2301_80350265/article/details/145096011
  • [技术干货] Java 中的泛型(超全详解)
    一、泛型概述1. 什么是泛型?为什么要使用泛型?泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错 那参数化类型是什么?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。 泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。 //没有泛型的时候,集合如何存储数据//结论://如果我们没有给集合指定类型,默认认为所有的数据类型都是Object类型//此时可以往集合添加任意的数据类型。//带来一个坏处:我们在获取数据的时候,无法使用他的特有行为。 //此时推出了泛型,可以在添加数据的时候就把类型进行统一。//而且我们在获取数据的时候,也省的强转了,非常的方便。2. 泛型使用场景在 ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合。 public class demo1 {    public static void main(String[] args) {        ArrayList<String> list=new ArrayList<>();        list.add("a");        list.add("b");        list.add("c");        for(String s:list){            System.out.println(s);        }    }}上面代码没有任何问题,在遍历 ArrayList 集合时,只需将 Object 对象进行向下转型成 String 类型即可得到 String 类型对象。但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?看下面代码:  public static void main(String[] args) {        ArrayList list = new ArrayList();        list.add("aaa");        list.add("bbb");        list.add("ccc");        list.add(666);         for (int i = 0; i < list.size(); i++) {            System.out.println((String)list.get(i));        }    }  上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Integer 对象不能强转为 String 类型。那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型了。 使用泛型代码如下:  public static void main(String[] args) {        ArrayList<String> list = new ArrayList();        list.add("aaa");        list.add("bbb");        list.add("ccc");        //list.add(666);// 在编译阶段,编译器会报错                for (int i = 0; i < list.size(); i++) {            System.out.println(list.get(i));        }    }< String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的 ClassCastException 异常的问题(这样体现了泛型的类型安全检测机制)。3.总结泛型的出现就是为了统一集合当中数据类型的   二、泛型类泛型类的定义  尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。 泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:   T :代表一般的任何类。  E :代表 Element 元素的意思,或者 Exception 异常的意思。  K :代表 Key 的意思。  V :代表 Value 的意思,通常与 K 一起配合使用。  S :代表 Subtype 的意思,文章后面部分会讲解示意。 自己实现集合 代码如下: package fangxing; import java.util.Arrays; public class MyArrayList<E> {    Object[] obj = new Object[10];    int size = 0;     /*    E: 表示不确定的类型,该类型在类名后面已经定义过了    e: 形参的名字,变量名     */    public boolean add(E e) {        obj[size++] = e;        return true;        //当添加成功以后,集合还是会把这些数据当做Object类型处理    }     public E get(int index) {        return (E) obj[index];        //获取的时候集合在把他强转<E>类型     }     @Override    public String toString() {           return Arrays.toString(obj);     }}package fangxing; import javax.xml.stream.events.StartDocument; public class demo3 {    public static void main(String[] args) {        MyArrayList<String> list = new MyArrayList<>();        list.add("aaa");        list.add("bbb");        list.add("ccc");        System.out.println(list);    }}三、泛型方法    格式     package fangxing; import java.util.ArrayList;import java.util.Arrays;import java.util.List; public class ListUtil {    private ListUtil() {    }     /*    参数一:集合    参数二: 最后要添加的元素     */    public static <E> void addAll(ArrayList<E> list, E e1, E e2) {        list.add(e1);        list.add(e2);    } } package fangxing; import java.util.ArrayList; public class demo4 {    public static void main(String[] args) {        ArrayList<String>list=new ArrayList<>();        ListUtil.addAll(list,"zhangsan","lisi");        System.out.println(list);//[zhangsan, lisi]     }}  添加很多元素 public static <E> void addAll(ArrayList<E> list, E ...e1) {        for (E e : e1) {          list.add(e);        }四、泛型接口  方法1:实现类给出具体类型 举例: public class MyArrayList2  implements List<String> public static void main(String[] args) {        MyArrayList2 list2=new MyArrayList2();    }方法2: 实现类延续泛型,创建对象再确定 public class MyArrayList3 <E>  implements List<E>  MyArrayList3<String> list = new MyArrayList3<>();五、类型擦除1. 什么是类型擦除 泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。 换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。 其实Java中的泛型本质是伪泛型 当把集合定义为string类型的时候,当数据添加在集合当中的时候,仅仅在门口检查了一下数据是否符合String类型,  如果是String类型,就添加成功,当添加成功以后,集合还是会把这些数据当做Object类型处理,当往外获取的时候,集合在把他强转String类型  当代码编译到class文件的时候,泛型就消失,叫泛型的擦除 看一个例子,假如我们给 ArrayList 集合传入两种不同的数据类型,并比较它们的类信息。 public class GenericType {    public static void main(String[] args) {          ArrayList<String> arrayString = new ArrayList<String>();           ArrayList<Integer> arrayInteger = new ArrayList<Integer>();           System.out.println(arrayString.getClass() == arrayInteger.getClass());// true    }  }在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。 明明我们在 <> 中传入了两种不同的数据类型,那为什么它们的类信息还是相同呢? 这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Objec t>类型。 那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢? 答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(即泛型通配符,后面我们会详细解释 2. 类型擦除的原理假如我们定义了一个 ArrayList< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问: 不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢? 换而言之,我们虽然定义了 ArrayList< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢? Java 是如何解决这个问题的? 其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。 可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制:以 ArrayList< Integer> 泛型集合为例。 当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型;当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)。在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)。  举例如下: public class GenericType {    public static void main(String[] args) {          ArrayList<Integer> arrayInteger = new ArrayList<Integer>();// 设置验票系统           arrayInteger.add(111);// 观众进场,验票系统验票,门票会被收走(类型擦除)        Integer n = arrayInteger.get(0);// 获取观众信息,编译器会进行强制类型转换        System.out.println(n);    }  }擦除 ArrayList< Integer > 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令: 对原始方法 get() 的调用,返回的是 Object 类型;将返回的 Object 类型强制转换为 Integer 类型; 代码如下: Integer n = arrayInteger.get(0);// 这条代码底层如下: //(1)get() 方法的返回值返回的是 Object 类型Object object = arrayInteger.get(0);//(2)编译器自动插入 Integer 的强制类型转换Integer n = (Integer) object;3. 类型擦除小结 1.泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析); 2.在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。 六、泛型通配符1. 泛型的继承泛型不具备继承性,但是数据具备继承性  此时,泛型里面写的什么类型,那么就传递什么类型的数据 泛型不具备继承性举例 package fangxing; import java.util.ArrayList; public class demo5 {    public static void main(String[] args) {        /*        泛型不具备继承性,但是数据具备继承性         */        ArrayList<Ye> list1=new ArrayList<>();        ArrayList<Fu> list2=new ArrayList<>();        ArrayList<Zi> list3=new ArrayList<>();        //调用method方法        method(list1);        //method(list2);//编译错误//method(list3);//编译错误    }    /*    此时,泛型里面写的什么类型,那么就传递什么类型的数据     */    public static  void method(ArrayList<Ye> list){    }}class Ye{}class Fu extends Ye{}class Zi extends Fu{}数据具备继承性     //数据具备继承性        list1.add(new Ye());//添加爷爷的对象等        list1.add(new Fu());        list1.add(new Zi());定义一个方法,形参是一个集合,但是集合中的数据类型不确定。应用场景:*      1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。*      2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符* 泛型的通配符:*      关键点:可以限定类型的范围。/* * 此时,泛型里面写的是什么类型,那么只能传递什么类型的数据。 * 弊端: *      利用泛型方法有一个小弊端,此时他可以接受任意的数据类型 *      Ye  Fu   Zi    Student * * 希望:本方法虽然不确定类型,但是以后我希望只能传递Ye Fu Zi * * 此时我们就可以使用泛型的通配符: *      ?也表示不确定的类型 *      他可以进行类型的限定 *      ? extends E: 表示可以传递E或者E所有的子类类型 *      ? super E:表示可以传递E或者E所有的父类类型 *举例 package fangxing; import java.util.ArrayList; /* *   需求: *       定义一个方法,形参是一个集合,但是集合中的数据类型不确定。 * * *//* * 此时,泛型里面写的是什么类型,那么只能传递什么类型的数据。 * 弊端: *      利用泛型方法有一个小弊端,此时他可以接受任意的数据类型 *      Ye  Fu   Zi    Student * * 希望:本方法虽然不确定类型,但是以后我希望只能传递Ye Fu Zi * * 此时我们就可以使用泛型的通配符: *      ?也表示不确定的类型 *      他可以进行类型的限定 *      ? extends E: 表示可以传递E或者E所有的子类类型 *      ? super E:表示可以传递E或者E所有的父类类型 * * 应用场景: *      1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。 *      2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符 * 泛型的通配符: *      关键点:可以限定类型的范围。 * * */public class demo6 {    public static void main(String[] args) {        //创建集合的对象        ArrayList<Ye> list1 = new ArrayList<>();        ArrayList<Fu> list2 = new ArrayList<>();        ArrayList<Zi> list3 = new ArrayList<>();        ArrayList<Student2> list4 = new ArrayList<>();        method(list1);        method(list2);        //method(list3);        //method(list4);    }    public static void method(ArrayList<? super Fu> list) {    }}class Ye {}class Fu extends Ye {}class Zi extends Fu {}class Student2{}2.练习/*      需求:          定义一个继承结构:                              动物                   |                           |                   猫                          狗                |      |                    |      |             波斯猫   狸花猫                泰迪   哈士奇           属性:名字,年龄           行为:吃东西                 波斯猫方法体打印:一只叫做XXX的,X岁的波斯猫,正在吃小饼干                 狸花猫方法体打印:一只叫做XXX的,X岁的狸花猫,正在吃鱼                 泰迪方法体打印:一只叫做XXX的,X岁的泰迪,正在吃骨头,边吃边蹭                 哈士奇方法体打印:一只叫做XXX的,X岁的哈士奇,正在吃骨头,边吃边拆家       测试类中定义一个方法用于饲养动物          public static void keepPet(ArrayList<???> list){              //遍历集合,调用动物的eat方法          }      要求1:该方法能养所有品种的猫,但是不能养狗      要求2:该方法能养所有品种的狗,但是不能养猫      要求3:该方法能养所有的动物,但是不能传递其他类型   */测试类 package lx; import java.util.ArrayList; public class demo1 {    /*          需求:              定义一个继承结构:                                  动物                       |                           |                       猫                          狗                    |      |                    |      |                 波斯猫   狸花猫                泰迪   哈士奇               属性:名字,年龄               行为:吃东西                     波斯猫方法体打印:一只叫做XXX的,X岁的波斯猫,正在吃小饼干                     狸花猫方法体打印:一只叫做XXX的,X岁的狸花猫,正在吃鱼                     泰迪方法体打印:一只叫做XXX的,X岁的泰迪,正在吃骨头,边吃边蹭                     哈士奇方法体打印:一只叫做XXX的,X岁的哈士奇,正在吃骨头,边吃边拆家          测试类中定义一个方法用于饲养动物              public static void keepPet(ArrayList<???> list){                  //遍历集合,调用动物的eat方法              }          要求1:该方法能养所有品种的猫,但是不能养狗          要求2:该方法能养所有品种的狗,但是不能养猫          要求3:该方法能养所有的动物,但是不能传递其他类型       */    public static void main(String[] args) {        HuskyDog h = new HuskyDog("哈士奇", 1);        LihuaCat l = new LihuaCat("狸花猫", 2);        PersianCat p = new PersianCat("波斯猫", 3);        TeddyDog t = new TeddyDog("泰迪", 4);        ArrayList<LihuaCat> list1 = new ArrayList<>();        ArrayList<PersianCat> list2 = new ArrayList<>();        // 向列表中添加一些猫的实例        list1.add(l);        list2.add(p);        //调用方法        keepPet1(list1);        keepPet1(list2);        System.out.println("-------------------------------------------");        ArrayList<HuskyDog> list3 = new ArrayList<>();        ArrayList<TeddyDog> list4 = new ArrayList<>();        // 向列表中添加一些狗的实例        list3.add(h);        list4.add(t);        //调用方法        keepPet2(list3);        keepPet2(list4);        System.out.println("-------------------------------------------");        list1.add(l);        list2.add(p);        list3.add(h);        list4.add(t);        keepPet3(list1);        keepPet3(list2);        keepPet3(list3);        keepPet3(list4);    }      /*    此时我们就可以使用泛型的通配符:      ?也表示不确定的类型      他可以进行类型的限定      ? extends E: 表示可以传递E或者E所有的子类类型     ? super E:表示可以传递E或者E所有的父类类型     */    //  要求1:该方法能养所有品种的猫,但是不能养狗    public static void keepPet1(ArrayList<? extends Cat> list) {        //遍历集合,调用动物的eat方法        for (Cat cat : list) {            cat.eat();        }    }    //  要求2:该方法能养所有品种的狗,但是不能养猫    public static void keepPet2(ArrayList<? extends Dog> list) {        //遍历集合,调用动物的eat方法        for (Dog dog : list) {            dog.eat();        }    }    //  要求3:该方法能养所有的动物,但是不能传递其他类型    public static void keepPet3(ArrayList<? extends Animal> list) {        //遍历集合,调用动物的eat方法        for (Animal animal : list) {            animal.eat();        }    }}Animal类 package lx; public abstract class Animal {    private String name;    private int age;     public Animal() {    }     public Animal(String name, int age) {        this.name = name;        this.age = age;    }     /**     * 获取     * @return name     */    public String getName() {        return name;    }     /**     * 设置     * @param name     */    public void setName(String name) {        this.name = name;    }     /**     * 获取     * @return age     */    public int getAge() {        return age;    }     /**     * 设置     * @param age     */    public void setAge(int age) {        this.age = age;    }     public String toString() {        return "Animal{name = " + name + ", age = " + age + "}";    }    public abstract  void eat();}cat类型 package lx; public abstract class Cat extends Animal{     public Cat() {    }     public Cat(String name, int age) {        super(name, age);    }}Dog类 package lx; public abstract class Dog extends Animal{    public Dog() {    }     public Dog(String name, int age) {        super(name, age);    }}哈士奇类 package lx; public class HuskyDog extends Dog{    @Override    public void eat() {        System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的哈士奇,正在吃骨头,边吃边拆家");     }     public HuskyDog() {    }     public HuskyDog(String name, int age) {        super(name, age);    }} 狸花猫类 package lx; public class LihuaCat extends Cat {    @Override    public void eat() {        System.out.println("一只叫做" + getName() + "的," + getAge() + "岁的狸花猫,正在吃鱼");    }     public LihuaCat() {    }     public LihuaCat(String name, int age) {        super(name, age);    }}波斯猫类 package lx; public class PersianCat extends Cat{    @Override    public void eat() {        System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的波斯猫,正在吃小饼干");    }     public PersianCat() {    }     public PersianCat(String name, int age) {        super(name, age);    }}泰迪猫类 package lx; public class TeddyDog extends Dog{    @Override    public void eat() {        System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁泰迪,正在吃骨头,边吃边蹭");    }     public TeddyDog() {    }     public TeddyDog(String name, int age) {        super(name, age);    }}————————————————           原文链接:https://blog.csdn.net/weixin_65752158/article/details/140321808
  • [技术干货] 入门篇:从0到1搭建 Java、Spring Boot、Spring Security 企业级权限管理系统
    前言本文基于 Java 和 Spring Boot 3,从 0 到 1 完成一个企业级后端项目的开发。依次整合 MySQL 和 Redis,实现基础的增删改查(CRUD)接口,并通过 Spring Security 完成登录认证与接口权限控制,最终构建完整的企业级安全管理框架。 作为开源项目youlai-boot 的入门篇,本文旨在帮助前端开发者或后端初学者快速上手 Java 后端开发。通过一步步实践,掌握项目的核心逻辑与实现细节,不仅能放心使用,还能轻松扩展和二次开发 环境准备本章节介绍安装 Java 开发所需的环境,包括 JDK、Maven 和 IntelliJ IDEA(简称 IDEA),这些工具是 Java 开发的核心环境。 安装 JDKJDK(Java Development Kit) 是 Java 开发工具包,包含编译器、运行时环境等,支持 Java 应用程序的开发与运行。 下载 JDK访问以下链接,下载最新版本的 JDK 安装包:https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe 安装 JDK下载完成后,双击安装包,根据引导完成安装。示例安装路径:D:\Java\jdk-17.0.3.1 配置环境变量打开 系统属性 -> 高级系统设置 -> 环境变量新建系统变量 JAVA_HOME,值为 D:\Java\jdk-17.0.3.1  在 Path 环境变量中,添加 %JAVA_HOME%\bin  验证安装在命令行中执行以下命令,查看 Java 版本: java -version 输出类似如下内容表示安装成功:   安装 MavenMaven 是一个流行的 Java 构建和依赖管理工具,类似于前端的 npm,用于管理项目的构建流程及第三方依赖库。 下载 Maven访问以下链接,下载最新的 bin.zip 文件:https://maven.apache.org/download.cgi 将 bin.zip 解压到本地目录,示例解压路径:D:\Soft\apache-maven-3.9.5 配置阿里云镜像编辑配置文件 D:\Soft\apache-maven-3.9.5\conf\settings.xml,在 <mirrors> 节点中添加以下配置: <mirrors>    <mirror>        <id>alimaven</id>        <name>aliyun maven</name>        <url>http://maven.aliyun.com/nexus/content/groups/public/</url>        <mirrorOf>central</mirrorOf>           </mirror></mirrors> 配置环境变量新建系统变量 M2_HOME,值为 D:\Soft\apache-maven-3.9.5  在 Path 环境变量中,添加 %M2_HOME%\bin  验证安装在命令行中执行以下命令,查看 Maven 版本: mvn -v1输出类似如下内容表示安装成功:   安装 IDEAIntelliJ IDEA 是一款功能强大的 Java 集成开发环境(IDE),由 JetBrains 开发,广泛用于 Java 项目的开发、调试和运行。 下载 IDEA访问以下链接,下载适合您系统的安装包:https://www.jetbrains.com/idea/download 安装 IDEA下载完成后,双击安装包,按引导完成安装即可。   具体的配置在创建项目之前说明。 安装 MySQL安装 MySQL 服务Windows 安装 MySQL 8:https://youlai.blog.csdn.net/article/details/133272887Linux 安装 MySQL 8:https://youlai.blog.csdn.net/article/details/130398179安装 MySQL 可视化工具推荐使用 Navicat,这是一款功能强大的数据库管理工具,但需要付费。如果你因未付费而遇到使用限制,可以选择 DBeaver 作为替代方案。 Navicat 官方下载地址:https://www.navicat.com.cn/download/navicat-premiumDBeaver 官方下载地址:https://dbeaver.io/下载并安装 Navicat 后,你将获得 14 天的免费试用期。安装完成后,连接到 MySQL 服务,即可对数据库和表进行可视化操作,体验非常流畅。 Navicat 界面效果:   安装 Redis安装 Redis 服务Windows 安装 Redis:https://youlai.blog.csdn.net/article/details/133410293Linux 安装 Redis:https://youlai.blog.csdn.net/article/details/130439335安装 Redis 可视化工具推荐使用开源的 AnotherRedisDesktopManager,这是一款功能强大且免费的 Redis 可视化工具。 AnotherRedisDesktopManager 下载地址:https://gitee.com/qishibo/AnotherRedisDesktopManager/releases  安装步骤: 下载安装程序(.exe 文件)。按照安装向导逐步操作,完成安装。使用步骤: 打开软件,点击“新建连接”。输入 Redis 服务器的连接信息(如主机地址、端口、密码等)。连接成功后,即可对 Redis 数据进行可视化操作。AnotherRedisDesktopManager 界面效果: 连接成功示例:  项目搭建新建项目打开 IDEA,选择 Projects → New Project。 项目名称:输入 youlai-boot(可根据实际需求调整)。项目类型:选择 Maven。JDK 版本:选择 JDK 17(确保已安装 JDK 17)。项目信息:Group:填写 com.youlai(可根据实际需求调整)。Artifact:填写 youlai-boot(可根据实际需求调整)。Package name:填写 com.youlai.boot(可根据实际需求调整)。 点击 Next,在左侧的依赖列表中勾选项目所需的依赖。 完成项目初始化后,项目结构如下 配置开发环境配置 JDK通过 File → Project Structure(快捷键 Ctrl + Alt + Shift + S)打开项目结构配置面板,确保 Project 和 Modules 使用的 SDK 版本为前面安装的 JDK 17。 配置 Maven通过 File → Settings(快捷键 Ctrl + Alt + S)打开设置面板,切换到 Maven 选项,并将 Maven 设置为前面安装到本地的版本。 验证配置在 IDEA 的 Terminal 中输入以下命令 mvn -v,验证 Maven 是否正确使用了 JDK 17: 快速开始创建第一个接口在 src/main/java 目录下的 com.youlai.boot 包中,新建一个名为 controller 的包。在 controller 包下创建一个名为 TestController 的 Java 类。在 TestController 类中添加一个简单的 hello-world 接口。  以下是 TestController 类的代码: /** * 测试接口 * * @author youlai */@RestControllerpublic class TestController {     @GetMapping("/hello-world")    public String test() {        return "hello world";    } } 启动测试在项目的右上角,点击 🐞 (手动绿色)图标以调试运行项目。   控制台显示 Tomcat 已在端口 8080 (http) ,表示应用成功启动   打开浏览器,访问 http://localhost:8080/hello-world,页面将显示 hello world,表示接口正常运行。   连接数据库为实现应用与 MySQL 的连接与操作,整合 MyBatis-Plus 可简化数据库操作,减少重复的 CRUD 代码,提升开发效率,实现高效、简洁、可维护的持久层开发。 创建数据库使用 MySQL 可视化工具 (Navicat) 执行下面脚本完成数据库的创建名为 youlai-boot 的数据库,其中包含测试的用户表     -- ----------------------------    -- 1. 创建数据库    -- ----------------------------    CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;     -- ----------------------------    -- 2. 创建表 && 数据初始化    -- ----------------------------    use youlai_boot; -- ----------------------------    -- Table structure for sys_user    -- ----------------------------    DROP TABLE IF EXISTS `sys_user`;    CREATE TABLE `sys_user` (        `id` int NOT NULL AUTO_INCREMENT,        `username` varchar(64) NULL DEFAULT NULL COMMENT '用户名',        `nickname` varchar(64) NULL DEFAULT NULL COMMENT '昵称',        `gender` tinyint(1) NULL DEFAULT 1 COMMENT '性别(1-男 2-女 0-保密)',        `password` varchar(100) NULL DEFAULT NULL COMMENT '密码',        `dept_id` int NULL DEFAULT NULL COMMENT '部门ID',        `avatar` varchar(255) NULL DEFAULT '' COMMENT '用户头像',        `mobile` varchar(20) NULL DEFAULT NULL COMMENT '联系方式',        `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',        `email` varchar(128) NULL DEFAULT NULL COMMENT '用户邮箱',        `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',        `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',        `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',        `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',        `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',        PRIMARY KEY (`id`) USING BTREE    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;     -- ----------------------------    -- Records of sys_user    -- ----------------------------    INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668888', 1, 'youlaitech@163.com', NULL, NULL, NULL, NULL, 0);    INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668887', 1, '', now(), NULL, now(), NULL, 0);    INSERT INTO `sys_user` VALUES (3, 'websocket', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668886', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);  添加依赖项目 pom.xml 添加 MySQL 驱动和 Mybatis-Plus 依赖: <!-- MySQL 8 驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>9.1.0</version><scope>runtime</scope></dependency> <!-- Druid 数据库连接池 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.24</version></dependency> <!-- MyBatis Plus Starter--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version></dependency> 配置数据源将 src/main/resources/application.properties 文件修改为 src/main/resources/application.yml,因为我们更倾向于使用 yml 格式。然后,在 yml 文件中添加以下内容: server:  port: 8080  spring:  datasource:    driver-class-name: com.mysql.cj.jdbc.Driver    url: jdbc:mysql://localhost:3306/youlai_boot?useSSL=false&serverTimezone=Asia/Shanghai&&characterEncoding=utf8    username: root    password: 123456 mybatis-plus:  configuration:    # 驼峰命名映射    map-underscore-to-camel-case: true    # 打印 sql 日志    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  global-config:    db-config:      id-type: auto # 主键策略      logic-delete-field: is_deleted # 全局逻辑删除字段(可选) 增删改查接口安装 MybatisX 插件在 IDEA 中依次点击 File → Settings(快捷键 Ctrl + Alt + S),打开设置面板,切换到 Plugins 选项卡,搜索 MybatisX 并安装插件。   自动代码生成在 IDEA 右侧导航栏点击 Database,打开数据库配置面板,选择新增数据源。   输入数据库的 主机地址、用户名 和 密码,测试连接成功后点击 OK 保存。  配置完数据源后,展开数据库中的表,右击 sys_user 表,选择 MybatisX-Generator 打开代码生成面板。   设置代码生成的目标路径,并选择 Mybatis-Plus 3 + Lombok 代码风格。  点击 Finish 生成,自动生成相关代码。 MybatisX 生成的代码存在以下问题: SysUserMapper.java 文件未标注 @Mapper 注解,导致无法被 Spring Boot 识别为 Mybatis 的 Mapper 接口。如果已配置 @MapperScan,可以省略此注解,但最简单的方法是直接在 SysUserMapper.java 文件中添加 @Mapper 注解。注意避免导入错误的包。  添加增删改查接口在 controller 包下创建 UserController.java,编写用户管理接口: /** * 用户控制层 * * @author youlai * @since 2024/12/04 */@RestController@RequestMapping("/users")@RequiredArgsConstructorpublic class UserController {     private final SysUserService userService;     /**     * 获取用户列表     */    @GetMapping    public List<SysUser> listUsers() {        return userService.list();    }     /**     * 获取用户详情     */    @GetMapping("/{id}")    public SysUser getUserById(@PathVariable Long id) {        return userService.getById(id);    }     /**     * 新增用户     */    @PostMapping    public String createUser(@RequestBody SysUser user) {        userService.save(user);        return "用户创建成功";    }     /**     * 更新用户信息     */    @PutMapping("/{id}")    public String updateUser(@PathVariable Long id, @RequestBody SysUser user) {        userService.updateById(user);        return "用户更新成功";    }     /**     * 删除用户     */    @DeleteMapping("/{id}")    public String deleteUser(@PathVariable Long id) {        userService.removeById(id);        return "用户删除成功";    } } 接口测试重新启动应用,在浏览器中访问 http://localhost:8080/users,查看用户数据。   其他增删改接口可以通过后续整合接口文档进行测试。 集成 Knife4j 接口文档Knife4j 是基于 Swagger2 和 OpenAPI3 的增强解决方案,旨在提供更友好的界面和更多功能扩展,帮助开发者更便捷地调试和测试 API。以下是通过参考 Knife4j 官方文档 Spring Boot 3 整合 Knife4j 实现集成的过程。 添加依赖在 pom.xml 文件中引入 Knife4j 的依赖: <dependency>    <groupId>com.github.xiaoymin</groupId>    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>    <version>4.5.0</version></dependency> 配置接口文档在 application.yml 文件中进行配置。注意,packages-to-scan 需要配置为项目的包路径,以确保接口能够被正确扫描,其他配置保持默认即可。 # springdoc-openapi 项目配置springdoc:  swagger-ui:    path: /swagger-ui.html    tags-sorter: alpha    operations-sorter: alpha  api-docs:    path: /v3/api-docs  group-configs:    - group: 'default'      paths-to-match: '/**'      packages-to-scan: com.youlai.boot.controller # 需要修改成自己项目的接口包路径# knife4j的增强配置,不需要增强可以不配knife4j:  enable: true  # 是否为生产环境,true 表示生产环境,接口文档将被禁用  production: false  setting:    language: zh_cn # 设置文档语言为中文 添加接口文档配置,在 com.youlai.boot.config 添加 OpenApiConfig 接口文档配置 package com.youlai.boot.config; import io.swagger.v3.oas.models.Components;import io.swagger.v3.oas.models.OpenAPI;import io.swagger.v3.oas.models.info.Info;import io.swagger.v3.oas.models.security.SecurityRequirement;import io.swagger.v3.oas.models.security.SecurityScheme;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springdoc.core.customizers.GlobalOpenApiCustomizer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.env.Environment;import org.springframework.http.HttpHeaders; /** * OpenAPI 接口文档配置 * * @author youlai */@Configuration@RequiredArgsConstructor@Slf4jpublic class OpenApiConfig {     private final Environment environment;     /**     * 接口信息     */     @Bean    public OpenAPI openApi() {         String appVersion = environment.getProperty("project.version", "1.0.0");         return new OpenAPI()                .info(new Info()                        .title("系统接口文档")                        .version(appVersion)                )                // 配置全局鉴权参数-Authorize                .components(new Components()                        .addSecuritySchemes(HttpHeaders.AUTHORIZATION,                                new SecurityScheme()                                        .name(HttpHeaders.AUTHORIZATION)                                        .type(SecurityScheme.Type.APIKEY)                                        .in(SecurityScheme.In.HEADER)                                        .scheme("Bearer")                                        .bearerFormat("JWT")                        )                );    }      /**     * 全局自定义扩展     * <p>     * 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。     * 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。     */    @Bean    public GlobalOpenApiCustomizer globalOpenApiCustomizer() {        return openApi -> {            // 全局添加鉴权参数            if (openApi.getPaths() != null) {                openApi.getPaths().forEach((s, pathItem) -> {                    // 登录接口/验证码不需要添加鉴权参数                    if ("/api/v1/auth/login".equals(s)) {                        return;                    }                    // 接口添加鉴权参数                    pathItem.readOperations()                            .forEach(operation ->                                    operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))                            );                });            }        };    } } 完善接口文档完善接口描述 在已有的 REST 接口中,使用 OpenAPI 规范注解来描述接口的详细信息,以便通过 Knife4j 生成更加清晰的接口文档。以下是如何为用户的增删改查接口添加文档描述注解的示例: @Tag(name = "用户接口")@RestController@RequestMapping("/users")@RequiredArgsConstructorpublic class UserController {     private final SysUserService userService;     @Operation(summary = "获取用户列表")    @GetMapping    public List<SysUser> listUsers() {        return userService.list();    }     @Operation(summary = "获取用户详情")    @GetMapping("/{id}")    public SysUser getUserById(            @Parameter(description = "用户ID") @PathVariable Long id    ) {        return userService.getById(id);    }     @Operation(summary = "新增用户")    @PostMapping    public String createUser(@RequestBody SysUser user) {        userService.save(user);        return "新增用户成功";    }     @Operation(summary = "修改用户")    @PutMapping("/{id}")    public String updateUser(            @Parameter(description = "用户ID") @PathVariable Long id,            @RequestBody SysUser user    ) {        userService.updateById(user);        return "修改用户成功";    }     @Operation(summary = "删除用户")    @DeleteMapping("/{id}")    public String deleteUser(            @Parameter(description = "用户ID") @PathVariable Long id    ) {        userService.removeById(id);        return "用户删除成功";    } } 完善实体类描述 在 SysUser 实体类中为每个字段添加 @Schema 注解,用于在接口文档中显示字段的详细说明及示例值: @Schema(description = "用户对象")@TableName(value = "sys_user")@Datapublic class SysUser implements Serializable {     @Schema(description = "用户ID", example = "1")    @TableId(type = IdType.AUTO)    private Integer id;     @Schema(description = "用户名", example = "admin")    private String username;     @Schema(description = "昵称", example = "管理员")    private String nickname;     @Schema(description = "性别(1-男,2-女,0-保密)", example = "1")    private Integer gender;     @Schema(description = "用户头像URL", example = "https://example.com/avatar.png")    private String avatar;     @Schema(description = "联系方式", example = "13800000000")    private String mobile;     @Schema(description = "用户邮箱", example = "admin@example.com")    private String email;        // ... } 使用接口文档完成以上步骤后,重新启动应用并访问生成的接口文档。 Swagger UI 文档地址:http://localhost:8080/swagger-ui/index.htmlKnife4j 文档地址:http://localhost:8080/doc.html通过左侧的接口列表查看增删改查接口,并点击具体接口查看详细参数说明及示例值:   接着,可以通过接口文档新增用户,接口返回成功后,可以看到数据库表中新增了一条用户数据:   集成 Redis 缓存Redis 是当前广泛使用的高性能缓存中间件,能够显著提升系统性能,减轻数据库压力,几乎成为现代应用的标配。 添加依赖在 pom.xml 文件中添加 Spring Boot Redis 依赖: <dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-redis</artifactId></dependency> 配置 Redis 连接在 application.yml 文件中配置 Redis 连接信息: spring:  data:    redis:      database: 0    # Redis 数据库索引      host: localhost  # Redis 主机地址      port: 6379  # Redis 端口      # 如果Redis 服务未设置密码,需要将password删掉或注释,而不是设置为空字符串      password: 123456      timeout: 10s 自定义序列化Spring Boot 默认使用 JdkSerializationRedisSerializer 进行序列化。我们可以通过自定义 RedisTemplate,将其修改为更易读的 String 和 JSON 序列化方式: /** *  Redis 自动装配配置 * * @author youlai * @since 2024/12/5 */@Configurationpublic class RedisConfig {     /**     * 自定义 RedisTemplate     * <p>     * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer     *     * @param redisConnectionFactory {@link RedisConnectionFactory}     * @return {@link RedisTemplate}     */    @Bean    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {         RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();        redisTemplate.setConnectionFactory(redisConnectionFactory);         redisTemplate.setKeySerializer(RedisSerializer.string());        redisTemplate.setValueSerializer(RedisSerializer.json());         redisTemplate.setHashKeySerializer(RedisSerializer.string());        redisTemplate.setHashValueSerializer(RedisSerializer.json());         redisTemplate.afterPropertiesSet();        return redisTemplate;    } } 单元测试在 src/test/java 目录的 com.youlai.boot 包下创建 RedisTests 单元测试类,用于验证数据的存储与读取。 @SpringBootTest@Slf4jclass RedisTests {     @Autowired    private RedisTemplate<String, Object> redisTemplate;     @Autowired    private SysUserService userService;     @Test    void testSetAndGet() {        Long userId = 1L;        // 1. 从数据库中获取用户信息        SysUser user = userService.getById(userId);        log.info("从数据库中获取用户信息: {}", user);         // 2. 将用户信息缓存到 Redis        redisTemplate.opsForValue().set("user:" + userId, user);         // 3. 从 Redis 中获取缓存的用户信息        SysUser cachedUser = (SysUser) redisTemplate.opsForValue().get("user:" + userId);        log.info("从 Redis 中获取用户信息: {}", cachedUser);    }} 点击测试类方法左侧的图标运行单元测试。   运行后,稍等片刻,若控制台成功打印从 Redis 获取的用户信息,则表示 Spring Boot 已成功集成 Redis。   完善 Web 框架统一响应处理为什么需要统一响应? 默认接口返回的数据结构仅包含业务数据,缺少状态码和提示信息,无法清晰表达操作结果。通过统一封装响应结构,可以提升接口的规范性,便于前后端协同开发和快速定位问题。 下图展示了不规范与规范响应数据的对比:左侧是默认返回的非标准数据,右侧是统一封装后的规范数据。   定义统一业务状态码 在 com.youlai.boot.common.result 包下创建 ResultCode 枚举,错误码规范参考 阿里开发手册-错误码设计。 package com.youlai.boot.common.result; import java.io.Serializable;import lombok.Getter; /** * 统一业务状态码枚举 * * @author youlai */@Getterpublic enum ResultCode implements Serializable {     SUCCESS("00000", "操作成功"),    TOKEN_INVALID("A0230", "Token 无效或已过期"),    ACCESS_UNAUTHORIZED("A0301", "访问未授权"),    SYSTEM_ERROR("B0001", "系统错误");     private final String code;    private final String message;     ResultCode(String code, String message) {        this.code = code;        this.message = message;    }} 创建统一响应结构 定义 Result 类,封装响应码、消息和数据。 package com.youlai.boot.common.result; import lombok.Data;import java.io.Serializable; /** * 统一响应结构 * * @author youlai **/@Datapublic class Result<T> implements Serializable {    // 响应码    private String code;    // 响应数据    private T data;    // 响应信息    private String msg;     /**     * 成功响应     */    public static <T> Result<T> success(T data) {        Result<T> result = new Result<>();        result.setCode(ResultCode.SUCCESS.getCode());        result.setMsg(ResultCode.SUCCESS.getMsg());        result.setData(data);        return result;    }     /**     * 失败响应     */    public static <T> Result<T> failed(ResultCode resultCode) {        Result<T> result = new Result<>();        result.setCode(resultCode.getCode());        result.setMsg(resultCode.getMsg());        return result;    }     /**     * 失败响应(系统默认错误)     */    public static <T> Result<T> failed() {        Result<T> result = new Result<>();        result.setCode(ResultCode.SYSTEM_ERROR.getCode());        result.setMsg(ResultCode.SYSTEM_ERROR.getMsg());        return result;    } }封装接口返回结果 调整接口代码,返回统一的响应格式。 @Operation(summary = "获取用户详情")@GetMapping("/{id}")public Result<SysUser> getUserById(    @Parameter(description = "用户ID") @PathVariable Long id) {    SysUser user = userService.getById(id);    return Result.success(user);} 效果预览 接口返回结构变为标准格式:   通过以上步骤,接口响应数据已完成统一封装,具备良好的规范性和可维护性,有助于前后端协同开发与错误定位。 全局异常处理为什么需要全局异常处理 如果没有统一的异常处理机制,抛出的业务异常和系统异常会以非标准格式返回,给前端的数据处理和问题排查带来困难。为了规范接口响应数据格式,需要引入全局异常处理。 以下接口模拟了一个业务逻辑中的异常: @Operation(summary = "获取用户详情")@GetMapping("/{id}")public Result<SysUser> getUserById(    @Parameter(description = "用户ID") @PathVariable Long id) {    // 模拟异常    int i = 1 / 0;     SysUser user = userService.getById(id);    return Result.success(user);} 当发生异常时,默认返回的数据格式如下所示:   这类非标准的响应格式既不直观,也不利于前后端协作。 全局异常处理器 在 com.youlai.boot.common.exception 包下创建全局异常处理器,用于捕获和处理系统异常。 package com.youlai.boot.common.exception; import com.youlai.boot.common.result.Result;import lombok.extern.slf4j.Slf4j;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理器 * * @author youlai */@RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler {     /**     * 处理系统异常     * <p>     * 兜底异常处理,处理未被捕获的异常     */    @ExceptionHandler(Exception.class)    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)    public <T> Result<T> handleNullPointerException(Exception e) {        log.error(e.getMessage(), e);        return Result.failed("系统异常:" + e.getMessage());    } } 验证全局异常处理 再次访问用户接口 localhost:8080/users/1 ,可以看到响应已经包含状态码和提示信息,数据格式变得更加规范:   自定义业务异常 在实际开发中,可能需要对特定的业务异常进行处理。通过自定义异常类 BusinessException,可以实现更灵活的异常处理机制。 package com.youlai.boot.common.exception; import com.youlai.boot.common.result.ResultCode;import lombok.Getter; /** * 自定义业务异常 * * @author youlai */@Getterpublic class BusinessException extends RuntimeException {     public ResultCode resultCode;     public BusinessException(ResultCode errorCode) {        super(errorCode.getMsg());        this.resultCode = errorCode;    }     public BusinessException(String message) {        super(message);    } } 在全局异常处理器中添加业务异常处理逻辑 @RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler {     /**     * 处理自定义业务异常     */    @ExceptionHandler(BusinessException.class)    public <T> Result<T> handleBusinessException(BusinessException e) {        log.error(e.getMessage(), e);        if(e.getResultCode()!=null){            return Result.failed(e.getResultCode());        }        return Result.failed(e.getMessage());    } } 模拟业务异常     @Operation(summary = "获取用户详情")    @GetMapping("/{id}")    public Result<SysUser> getUserById(            @Parameter(description = "用户ID") @PathVariable Long id    ) {        SysUser user = userService.getById(-1);        // 模拟异常        if (user == null) {            throw new BusinessException("用户不存在");        }        return Result.success(user);    }  请求不存在的用户时,响应如下:   通过全局异常处理的引入和自定义业务异常的定义,接口的响应数据得以标准化,提升了前后端协作的效率和系统的可维护性。 日志输出配置日志作为企业级应用项目中的重要一环,不仅是调试问题的关键手段,更是用户问题排查和争议解决的强有力支持工具 配置 logback-spring.xml 日志文件 在 src/main/resources 目录下,新增 logback-spring.xml 配置文件。基于 Spring Boot “约定优于配置” 的设计理念,项目默认会自动加载并使用该配置文件。 <?xml version="1.0" encoding="UTF-8"?><!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 --><configuration>     <!-- SpringBoot默认logback的配置 -->    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>     <springProperty scope="context" name="APP_NAME" source="spring.application.name"/>    <property name="LOG_HOME" value="/logs/${APP_NAME}"/>     <!-- 1. 输出到控制台-->    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">        <!-- <withJansi>true</withJansi>-->        <!--此日志appender是为开发使用,只配置最低级别,控制台输出的日志级别是大于或等于此级别的日志信息-->        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">            <level>DEBUG</level>        </filter>        <encoder>            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>            <charset>UTF-8</charset>        </encoder>    </appender>     <!-- 2. 输出到文件  -->    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">        <!-- 当前记录的日志文档完整路径 -->        <file>${LOG_HOME}/log.log</file>        <encoder>            <!--日志文档输出格式-->            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} -%5level ---[%15.15thread] %-40.40logger{39} : %msg%n%n</pattern>            <charset>UTF-8</charset>        </encoder>        <!-- 日志记录器的滚动策略,按大小和时间记录 -->        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">            <!-- 滚动后的日志文件命名模式 -->            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}.%i.log</fileNamePattern>            <!-- 单个日志文件的最大大小 -->            <maxFileSize>10MB</maxFileSize>            <!-- 最大保留30天的日志 -->            <maxHistory>30</maxHistory>            <!-- 总日志文件大小不超过3GB -->            <totalSizeCap>1GB</totalSizeCap>        </rollingPolicy>        <!-- 临界值过滤器,输出大于INFO级别日志 -->        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">            <level>INFO</level>        </filter>    </appender>     <!-- 根日志记录器配置 -->    <root level="INFO">        <!-- 引用上面定义的两个appender,日志将同时输出到控制台和文件 -->        <appender-ref ref="CONSOLE"/>        <appender-ref ref="FILE"/>    </root></configuration> 查看日志输出效果 添加配置文件后,启动项目并触发相关日志行为,控制台和日志文件会同时输出日志信息:   集成 Spring SecuritySpring Security 是一个强大的安全框架,可用于身份认证和权限管理。 添加依赖在 pom.xml 添加 Spring Security 依赖 <dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-security</artifactId></dependency> 获取用户认证信息从数据库获取用户信息(用户名、密码、角色),用于和前端输入的用户名密码做判读,如果认证成功,将角色权限信息绑定到用户会话,简单概括就是提供给认证授权的用户信息。 定义用户认证信息类 UserDetails 创建 com.youlai.boot.security.model 包,新建 SysUserDetails 用户认证信息对象,继承 Spring Security 的 UserDetails 接口 /** * Spring Security 用户认证信息对象 * <p> * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 * * @author youlai */@Data@NoArgsConstructorpublic class SysUserDetails implements UserDetails {     /**     * 用户ID     */    private Integer userId;     /**     * 用户名     */    private String username;     /**     * 密码     */    private String password;     /**     * 账号是否启用(true:启用,false:禁用)     */    private Boolean enabled;     /**     * 用户角色权限集合     */    private Collection<SimpleGrantedAuthority> authorities;     /**     * 根据用户认证信息初始化用户详情对象     */    public SysUserDetails(SysUser user) {        this.userId = user.getId();        this.username = user.getUsername();        this.password = user.getPassword();        this.enabled = ObjectUtil.equal(user.getStatus(), 1);         // 初始化角色权限集合        this.authorities = CollectionUtil.isNotEmpty(user.getRoles())                ? user.getRoles().stream()                // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (sys:user:add)                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))                .collect(Collectors.toSet())                : Collections.emptySet();    }     @Override    public Collection<? extends GrantedAuthority> getAuthorities() {        return this.authorities;    }     @Override    public String getPassword() {        return this.password;    }     @Override    public String getUsername() {        return this.username;    }     @Override    public boolean isEnabled() {        return this.enabled;    }} 获取用户认证信息服务类 创建 com.youlai.boot.security.service 包,新建 SysUserDetailsService 用户认证信息加载服务类,继承 Spring Security 的 UserDetailsService 接口 /** * 用户认证信息加载服务类 * <p> * 在用户登录时,Spring Security 会自动调用该类的 {@link #loadUserByUsername(String)} 方法, * 获取封装后的用户信息对象 {@link SysUserDetails},用于后续的身份验证和权限管理。 * * @author youlai */@Service@RequiredArgsConstructorpublic class SysUserDetailsService implements UserDetailsService {     private final SysUserService userService;     /**     * 根据用户名加载用户的认证信息     */    @Override    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {        // 查询用户基本信息        SysUser user = userService.getOne(new LambdaQueryWrapper<SysUser>()                .eq(SysUser::getUsername, username)        );        if (user == null) {            throw new UsernameNotFoundException(username);        }        // 模拟设置角色,实际应从数据库获取用户角色信息        Set<String> roles = Set.of("ADMIN");        user.setRoles(roles);         // 模拟设置权限,实际应从数据库获取用户权限信息        Set<String> perms = Set.of("sys:user:query");        user.setPerms(perms);         // 将数据库中查询到的用户信息封装成 Spring Security 需要的 UserDetails 对象        return new SysUserDetails(user);    }} 认证鉴权异常处理在 com.youlai.boot.common.util 添加响应工具类 ResponseUtils @Slf4jpublic class ResponseUtils {     /**     * 异常消息返回(适用过滤器中处理异常响应)     *     * @param response  HttpServletResponse     * @param resultCode 响应结果码     */    public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {        // 根据不同的结果码设置HTTP状态        int status = switch (resultCode) {            case ACCESS_UNAUTHORIZED,             ACCESS_TOKEN_INVALID                     -> HttpStatus.UNAUTHORIZED.value();            default -> HttpStatus.BAD_REQUEST.value();        };         response.setStatus(status);        response.setContentType(MediaType.APPLICATION_JSON_VALUE);        response.setCharacterEncoding(StandardCharsets.UTF_8.name());         try (PrintWriter writer = response.getWriter()) {            String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));            writer.print(jsonResponse);            writer.flush(); // 确保将响应内容写入到输出流        } catch (IOException e) {            log.error("响应异常处理失败", e);        }    } } 功能 AuthenticationEntryPoint AccessDeniedHandler对应异常 AuthenticationException AccessDeniedException适用场景 用户未认证(无凭证或凭证无效) 用户已认证但无权限返回 HTTP 状态码 401 Unauthorized 403 Forbidden常见使用位置 用于处理身份认证失败的全局入口逻辑 用于处理权限不足时的逻辑用户未认证处理器 /** * 未认证处理器 * * @author youlai */@Slf4jpublic class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {     @Override    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {        if (authException instanceof BadCredentialsException) {            // 用户名或密码错误            ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR);        } else {            // token 无效或者 token 过期            ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);        }    }    } 无权限访问处理器 /** * 无权限访问处理器 * * @author youlai */public class MyAccessDeniedHandler implements AccessDeniedHandler {     @Override    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {        ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);    } } 注意事项 在全局异常处理器中,认证异常(AuthenticationException)和授权异常(AccessDeniedException)不应被捕获,否则这些异常将无法交给 Spring Security 的异常处理机制进行处理。因此,当捕获到这类异常时,应该将其重新抛出,交给 Spring Security 来处理其特定的逻辑。 public class GlobalExceptionHandler {     /**     * 处理系统异常     * <p>     * 兜底异常处理,处理未被捕获的异常     */    @ExceptionHandler(Exception.class)    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)    public <T> Result<T> handleNullPointerException(Exception e) throws Exception {        // 如果是 Spring Security 的认证异常或授权异常,直接抛出,交由 Spring Security 的异常处理器处理        if (e instanceof AccessDeniedException                || e instanceof AuthenticationException) {            throw e;        }         log.error(e.getMessage(), e);        return Result.failed("系统异常,请联系管理员");    } } 认证授权配置在 com.youlai.boot.config 包下新建 SecurityConfig 用来 Spring Security 安全配置 /** * Spring Security 安全配置 * * @author youlai */@Configuration@EnableWebSecurity  // 启用 Spring Security 的 Web 安全功能,允许配置安全过滤链@EnableMethodSecurity // 启用方法级别的安全控制(如 @PreAuthorize 等)public class SecurityConfig {     /**     * 忽略认证的 URI 地址     */    private final String[] IGNORE_URIS = {"/api/v1/auth/login"};     /**     * 配置安全过滤链,用于定义哪些请求需要认证或授权     */    @Bean    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {         // 配置认证与授权规则        http                .authorizeHttpRequests(requestMatcherRegistry ->                        requestMatcherRegistry                                .requestMatchers(IGNORE_URIS).permitAll() // 登录接口无需认证                                .anyRequest().authenticated() // 其他请求必须认证                )                // 使用无状态认证,禁用 Session 管理(前后端分离 + JWT)                .sessionManagement(configurer ->                        configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)                )                // 禁用 CSRF 防护(前后端分离通过 Token 验证,不需要 CSRF)                .csrf(AbstractHttpConfigurer::disable)                // 禁用默认的表单登录功能                .formLogin(AbstractHttpConfigurer::disable)                // 禁用 HTTP Basic 认证(统一使用 JWT 认证)                .httpBasic(AbstractHttpConfigurer::disable)                // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中                .headers(headers ->                        headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)                )                // 异常处理                .exceptionHandling(configurer -> {                    configurer                            .authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证处理器                            .accessDeniedHandler(new MyAccessDeniedHandler()); // 无权限访问处理器                });                        ;         return http.build();    }     /**     * 配置密码加密器     *     * @return 密码加密器     */    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }         /**     * 用于配置不需要认证的 URI 地址     */    @Bean    public WebSecurityCustomizer webSecurityCustomizer() {        return (web) -> {            web.ignoring().requestMatchers(                    "/v3/api-docs/**",                    "/swagger-ui/**",                    "/swagger-ui.html",                    "/webjars/**",                    "/doc.html"            );        };    }     /**     *认证管理器     */    @Bean    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {        return configuration.getAuthenticationManager();    }} Token 工具类在 com.youlai.boot.security.manager 包下新建 JwtTokenManager ,用于生成和解析 token /** * JWT Token 管理类 * * @author youlai */@Servicepublic class JwtTokenManager {     /**     * JWT 密钥,用于签名和解签名     */    private final String secretKey = " SecretKey012345678901234567890123456789012345678901234567890123456789";     /**     * 访问令牌有效期(单位:秒), 默认 1 小时     */    private final Integer accessTokenTimeToLive = 3600;     /**     *  生成 JWT 访问令牌 - 用于登录认证成功后生成 JWT Token     *     * @param authentication 用户认证信息     * @return JWT 访问令牌     */    public String generateToken(Authentication authentication) {        SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();        Map<String, Object> payload = new HashMap<>();        // 将用户 ID 放入 JWT 载荷中, 如有其他扩展字段也可以放入        payload.put("userId", userDetails.getUserId());         // 将用户的角色和权限信息放入 JWT 载荷中,例如:["ROLE_ADMIN", "sys:user:query"]        Set<String> authorities = authentication.getAuthorities().stream()                .map(GrantedAuthority::getAuthority)                .collect(Collectors.toSet());        payload.put("authorities", authorities);         Date now = new Date();        payload.put(JWTPayload.ISSUED_AT, now);         // 设置过期时间 -1 表示永不过期        if (accessTokenTimeToLive != -1) {            Date expiresAt = DateUtil.offsetSecond(now, accessTokenTimeToLive);            payload.put(JWTPayload.EXPIRES_AT, expiresAt);        }        payload.put(JWTPayload.SUBJECT, authentication.getName());        payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());         return JWTUtil.createToken(payload, secretKey.getBytes());    }      /**     * 解析 JWT Token 获取 Authentication 对象 - 用于接口请求时解析 JWT Token 获取用户信息     *     * @param token JWT Token     * @return Authentication 对象     */    public Authentication parseToken(String token) {         JWT jwt = JWTUtil.parseToken(token);        JSONObject payloads = jwt.getPayloads();        SysUserDetails userDetails = new SysUserDetails();        userDetails.setUserId(payloads.getInt("userId")); // 用户ID        userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名        // 角色集合        Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray("authorities")                .stream()                .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))                .collect(Collectors.toSet());         return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);    }     /**     *  验证 JWT Token 是否有效     *     * @param token JWT Token 不携带 Bearer 前缀     * @return 是否有效     */    public boolean validateToken(String token) {        JWT jwt = JWTUtil.parseToken(token);        // 检查 Token 是否有效(验签 + 是否过期)        return jwt.setKey(secretKey.getBytes()).validate(0);    } } 登录认证接口在 com.youlai.boot.controller 包下新建 AuthController /** * 认证控制器 * * @author youlai */@Tag(name = "01.认证中心")@RestController@RequestMapping("/api/v1/auth")@RequiredArgsConstructorpublic class AuthController {     // 认证管理器 - 用于执行认证    private final AuthenticationManager authenticationManager;     // JWT 令牌服务类 - 用于生成 JWT 令牌    private final JwtTokenManager jwtTokenManager;     @Operation(summary = "登录")    @PostMapping("/login")    public Result<String> login(            @Parameter(description = "用户名", example = "admin") @RequestParam String username,            @Parameter(description = "密码", example = "123456") @RequestParam String password    ) {         // 1. 创建用于密码认证的令牌(未认证)        UsernamePasswordAuthenticationToken authenticationToken =                new UsernamePasswordAuthenticationToken(username.trim(), password);         // 2. 执行认证(认证中)        Authentication authentication = authenticationManager.authenticate(authenticationToken);         // 3. 认证成功后生成 JWT 令牌(已认证)        String accessToken = jwtTokenManager.generateToken(authentication);         return Result.success(accessToken);    }} 访问本地接口文档 http://localhost:8080/doc.html 选择登录接口进行调试发送请求,输入用户名和密码,如果登录成功返回访问令牌 token   访问 https://jwt.io/ 解析返回的 token ,主要分为三部分 Header(头部) 、Payload(负载) 和 Signature(签名) ,其中负载除了固定字段之外,还出现自定义扩展的字段 userId。   访问鉴权我们拿获取用户列表举例,首先需要验证我们在上一步登录拿到的访问令牌 token 是否有效(验签、是否过期等),然后需要校验该用户是否有访问接口的权限,本节就围绕以上问题展开。 验证解析 Token 过滤器 新建 com.youlai.boot.security.filter 添加 JwtValidationFilter 过滤器 用于验证和解析token /** * JWT Token 验证和解析过滤器 * <p> * 负责从请求头中获取 JWT Token,验证其有效性并将用户信息设置到 Spring Security 上下文中。 * 如果 Token 无效或解析失败,直接返回错误响应。 * </p> * * @author youlai */public class JwtAuthenticationFilter extends OncePerRequestFilter {     private static final String BEARER_PREFIX = "Bearer ";     private final JwtTokenManager jwtTokenManager;     public JwtAuthenticationFilter(JwtTokenManager jwtTokenManager) {        this.jwtTokenManager = jwtTokenManager;    }     @Override    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {        String token = request.getHeader(HttpHeaders.AUTHORIZATION);        try {            if (StrUtil.isNotBlank(token) && token.startsWith(BEARER_PREFIX)) {                // 去除 Bearer 前缀                token = token.substring(BEARER_PREFIX.length());                // 校验 JWT Token ,包括验签和是否过期                boolean isValidate = jwtTokenService.validateToken(token);                if (!isValidate) {                    writeErrMsg(response, ResultCode.TOKEN_INVALID);                    return;                }                // 将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中                Authentication authentication = jwtTokenManager.parseToken(token);                SecurityContextHolder.getContext().setAuthentication(authentication);            }        } catch (Exception e) {            SecurityContextHolder.clearContext();            writeErrMsg(response, ResultCode.TOKEN_INVALID);            return;        }                // 无 Token 或 Token 验证通过时,继续执行过滤链。        // 如果请求不在白名单内(例如登录接口、静态资源等),        // 后续的 AuthorizationFilter 会根据配置的权限规则和安全策略进行权限校验。        // 例如:        // - 匹配到 permitAll() 的规则会直接放行。        // - 需要认证的请求会校验 SecurityContext 中是否存在有效的 Authentication。        // 若无有效 Authentication 或权限不足,则返回 403 Forbidden。        filterChain.doFilter(request, response);    }     /**     * 异常消息返回     *     * @param response  HttpServletResponse     * @param resultCode 响应结果码     */    public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {        int status = switch (resultCode) {            case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();            default -> HttpStatus.BAD_REQUEST.value();        };         response.setStatus(status);        response.setContentType(MediaType.APPLICATION_JSON_VALUE);        response.setCharacterEncoding(StandardCharsets.UTF_8.name());         try (PrintWriter writer = response.getWriter()) {            String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));            writer.print(jsonResponse);            writer.flush();        } catch (IOException e) {            // 日志记录:捕获响应写入失败异常            // LOGGER.error("Error writing response", e);        }    }} 添加 JWT 验证和解析过滤器 在 SecurityConfig 过滤器链添加 JWT token校验和解析成 Authentication 对象的过滤器。 /** * Spring Security 安全配置 * * @author youlai */@RequiredArgsConstructorpublic class SecurityConfig {     // JWT Token 服务 , 用于 Token 的生成、解析、验证等操作    private final JwtTokenManager jwtTokenManager;     @Bean    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {      return http                 // ...                 // JWT 验证和解析过滤器                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class)  .build();    }     // ...} 获取用户列表接口 @RestController@RequestMapping("/users")@RequiredArgsConstructorpublic class UserController {     private final SysUserService userService;     @Operation(summary = "获取用户列表")    @GetMapping    @PreAuthorize("hasAuthority('sys:user:query')")    public List<SysUser> listUsers() {        return userService.list();    }    }访问一个主要测试访问凭据令牌是否认证以及对应的用户是否有访问该接口所需的权限,上面获取用户信息列表的接口未配置在security的白名单中,也就是需要认证,且被 @PreAuthorize(“hasAuthority(‘sys:user:query’)”) 标记说明用户需要有 sys:user:query的权限,也就是所谓的鉴权。 正常访问  不携带 token 访问   携带错误/过期的 token   有访问权限 用户拥有的权限 sys:user:query public class UserController {     @Operation(summary = "获取用户列表")    @GetMapping    @PreAuthorize("hasAuthority('sys:user:query')")  // 需要 sys:user:query 权限    public List<SysUser> listUsers() {        return userService.list();    }    }   无访问权限 用户没有拥有的权限 sys:user:info———————————————— 原文链接:https://blog.csdn.net/u013737132/article/details/145177011
  • [技术干货] 详解Java之Spring MVC篇二
    理解CookieHTTP协议自身是“无状态”协议,但是在实际开发中,我们很多时候是需要知道请求之间的关联关系的。 上述图中的 "令牌" 通常就存储在 Cookie 字段中.此时在服务器这边就需要记录"令牌"信息, 以及令牌对应的⽤⼾信息, 这个就是 Session 机制所做的⼯作.理解Session我们先来了解⼀下什么是会话.会话: 对话的意思 在计算机领域, 会话是⼀个客⼾与服务器之间的不中断的请求响应. 对客⼾的每个请求,服务器能够识别出请求来⾃于同⼀个客⼾. 当⼀个未知的客⼾向Web应⽤程序发送第⼀个请求时就开始了⼀个会话.当客⼾明确结束会话或服务器在⼀个时限内没有接受到客⼾的任何请求时,会话就结束了.服务器同⼀时刻收到的请求是很多的. 服务器需要清楚的区分每个请求是从属于哪个⽤⼾, 也就是属于哪个会话, 就需要在服务器这边记录每个会话以及与⽤⼾的信息的对应关系.Session是服务器为了保存⽤⼾信息⽽创建的⼀个特殊的对象. Session的本质就是⼀个 "哈希表", 存储了⼀些键值对结构. Key 就是SessionID, Value 就是⽤⼾信息(⽤⼾信息可以根据需求灵活设计). SessionId是由服务器⽣成的⼀个 "唯⼀性字符串", 从 Session 机制的⻆度来看, 这个唯⼀性字符串称为 "SessionId". 但是站在整个登录流程中看待, 也可以把这个唯⼀性字符串称为 "token".上述例⼦中的令牌ID, 就可以看做是SessionId, 只不过令牌除了ID之外, 还会带⼀些其他信息, ⽐如时间, 签名等. 1. 当⽤⼾登陆的时候, 服务器在 Session 中新增⼀个新记录, 并把 sessionId返回给客⼾端. (通过HTTP 响应中的 Set-Cookie 字段返回).2. 客⼾端后续再给服务器发送请求的时候, 需要在请求中带上 sessionId. (通过 HTTP 请求中的Cookie 字段带上).3. 服务器收到请求之后, 根据请求中的 sessionId在 Session 信息中获取到对应的⽤⼾信息, 再进⾏后续操作.找不到则重新创建Session, 并把SessionID返回. Session 默认是保存在内存中的. 如果重启服务器则 Session 数据就会丢失.Cookie和Session的区别• Cookie 是客⼾端保存⽤⼾信息的⼀种机制. Session 是服务器端保存⽤⼾信息的⼀种机制.• Cookie 和 Session之间主要是通过 SessionId 关联起来的, SessionId 是 Cookie 和 Session 之间的桥梁• Cookie 和 Session 经常会在⼀起配合使⽤. 但是不是必须配合.• 完全可以⽤ Cookie 来保存⼀些数据在客⼾端. 这些数据不⼀定是⽤⼾⾝份信息, 也不⼀定是SessionId• Session 中的sessionId 也不需要⾮得通过 Cookie/Set-Cookie 传递, ⽐如通过URL传递.1. 存储位置Cookie:存储在客户端(浏览器)上。当服务器响应一个HTTP请求时,它可以在响应头中包含一个Set-Cookie字段,浏览器会保存这个Cookie,并在后续的请求中通过Cookie请求头将Cookie发送回服务器。Session:存储在服务器端。服务器为每个用户会话创建一个唯一的标识符(通常是Session ID),这个标识符被发送到客户端(通常是通过Cookie,但也可以通过URL重写等方式),客户端在后续的请求中携带这个标识符,服务器通过这个标识符来识别用户会话。2. 安全性Cookie:由于存储在客户端,因此相对容易受到攻击,如跨站脚本攻击(XSS)可以读取或修改Cookie。但是,可以通过设置HttpOnly和Secure标志来增加安全性,HttpOnly标志可以防止JavaScript访问Cookie,Secure标志则要求Cookie仅通过HTTPS发送。Session:存储在服务器端,因此相对更安全。但是,如果Session ID被泄露(例如,通过URL重写并泄露在日志中),则可能面临会话劫持的风险。3. 容量限制Cookie:由于存储在客户端,其大小受到浏览器和服务器设置的限制。大多数浏览器对每个Cookie的大小和每个域名下的Cookie总数都有限制。Session:存储在服务器端,因此其大小限制主要取决于服务器的内存和配置,通常远大于Cookie的限制。4. 生命周期Cookie:可以设置过期时间(Expires/Max-Age),也可以不设置(会话Cookie,浏览器关闭时失效)。Session:通常依赖于服务器端的配置和Session的存储方式(如内存、数据库等)。如果服务器配置了Session的超时时间,则Session在达到超时时间后会被销毁。5. 使用场景Cookie:适用于存储少量数据,如用户偏好设置、登录状态等。由于存储在客户端,可以跨多个页面和请求持久化数据。Session:适用于存储大量数据,如用户信息、购物车内容等。由于存储在服务器端,可以更安全地管理用户会话。获取Cookie首先先设置Cookie 再获取:    @RequestMapping("/getC")    public String getCookie(HttpServletRequest request){        //获取参数//        String name = request.getParameter("name");        Cookie[] cookies = request.getCookies();        if (cookies!=null){            Arrays.stream(cookies).forEach(ck -> System.out.println(ck.getName()+":"+ck.getValue()));        }        return "获取Cookie";    }更为简洁的代码:@RequestMapping("/getC2")public String getCookie2(@CookieValue("name") String name){    return "从Cookie中获取值, name:"+name;}获取SessionSession是服务器端的机制, 我们需要先存储, 才能再获取.Session 也是基于HttpServletRequest 来存储和获取的.设置Session    @RequestMapping("/setSess")    public String setSess(HttpServletRequest request){        //从cookie中获取到了sessionID, 根据sessionID获取Session对象, 如果没有获取到, 会创建一个session对象        HttpSession session = request.getSession();        session.setAttribute("name", "zhangsan");        return "设置session成功";    }再获取:    @RequestMapping("/getSess")    public String getSess(HttpServletRequest request){        //从cookie中获取到了sessionID, 根据session获取Session对象        HttpSession session = request.getSession();        String name = (String)session.getAttribute("name");        return "从session中获取name:"+name;    } 获取Session更为简洁的代码:    @RequestMapping("/getSess2")    public String getSess2(HttpSession session){        String name = (String)session.getAttribute("name");        return "从session中获取name:"+name;    }    @RequestMapping("/getSess3")    public String getSess3(@SessionAttribute("name") String name){        return "从session中获取name:"+name;    }获取Header获取User-Agent    @RequestMapping("/getHeader")    public String getHeader(HttpServletRequest request){        String userAgent = request.getHeader("User-Agent");        return "从header中获取信息, userAgent:"+userAgent;     }更为简洁的代码:    @RequestMapping("/getHeader2")    public String getHeader2(@RequestHeader("User-Agent") String userAgent){        return "从header中获取信息, userAgent:"+userAgent; ————————————————                 原文链接:https://blog.csdn.net/wmh_1234567/article/details/141640649
  • [技术干货] Java篇图书管理系统
    前言相信大家都有去过图书馆吧,那么在借阅图书和归还的时候,都有一个系统来记录并操作这些过程,所以今天就带着大家利用之前的所学知识来简单实现一下图书管理系统的基本逻辑构造一. 图书管理系统的核心图书管理系统的核心包括三个部分:书籍的信息(书本属性)、操作书籍的人(管理员和读者)、 对书籍的操作(借阅、归还)所以在这里我们分别创建3个包 book、operation、user 来实现各个部分的操作: 二. 图书管理系统基本框架2.1 book包我们在book这个包中创建2个类:Book、Booklist,Book用来描述书籍的基本信息,Booklist充当书架,里面用来记录书架中书籍的信息。 2.1.1 Book(书籍类)首先在Book中我们要创建变量来记录书籍的基本信息:    private String name; //书名    private String author; //作者    private  int price;  //价格    private String type; //书籍类型    private boolean isborrow; //是否被借出并且这里的书籍基本信息,我们是用private访问修饰限定符修饰变量的,其他类想要直接访问是访问不了的,所以这里需要我们创建方法间接访问。public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getAuthor() {        return author;    }    public void setAuthor(String author) {        this.author = author;    }    public int getPrice() {        return price;    }    public void setPrice(int price) {        this.price = price;    }    public String getType() {        return type;    }    public void setType(String type) {        this.type = type;    }    public boolean isIsborrow() {        return isborrow;    }    public void setIsborrow(boolean isborrow) {        this.isborrow = isborrow;    }接下来我们来写Book类的构造方法:那么当我们每次新增一本书的时候 ,该书籍默认就是未借出的状态,默认初始值就是false,所以不需要在构造方法中进行初始化public Book(String name, String author, int price, String type) {        //初始化书的属性        this.name = name;        this.author = author;        this.price = price;        this.type = type;    }这时候书籍的信息属性都能够被记录了,那么我们想看看这些书籍的信息该怎么办呢?没错打印出来,此时我们需要对 ToString 方法进行重写:  2.1.2 Booklist (书架类)能够记录书籍信息的Book类创建完成后,我们来创建一个书架类(Booklist),书架类的作用就是能够存放每一本书,并且记录每一本书的借阅状态。    private Book [] books;//表示书架可以存放10本书    private int usedbooks;//表示books数组中存放了几本书那么我们依然需要通过间接访问才能访问书架中的基本信息,需要自己提供方法间接访问:    public int getUsedbooks() {        return usedbooks;//返回有效书本数量    }    public void setUsedbooks(int usedbooks) {        this.usedbooks = usedbooks;//更改有效书本数量    }    public Book getbook(int pos){        return books[pos];  //返回书架中 第pos本书    }    public void setBooks(int pos,Book book){        this.books[pos]=book;  //更改书架中的书    }那么我们还是写一下Booklist类的构造方法,默认书架中已经存放了书籍:public BookList() {        this.books = new Book [10];  //默认创建存放10本书大小的数组        this.books[0]=new Book("三国演义","罗贯中",20,"四大名著");        this.books[1]=new Book("西游记","吴承恩",22,"四大名著");        this.books[2]=new Book("红楼梦","曹雪芹",24,"四大名著");        this.books[3]=new Book("水浒传","施耐庵",26,"四大名著");        this.usedbooks=4;//初始化有效书本数量为4本    } 那么在book这个包的操作就告一段落,后续随着思路的深入再进行补充~2.2 user包在user包中我们新建三个类来描述管理员和用户,那么管理员和用户也是拥有相同属性的,而第三个类User,我们就定义成抽象类,让管理员类与用户类去继承,这样就可以省略一些重复的代码啦2.2.1 User类那么管理员与用户共同拥有的特征就是都有名字,那么我们就在User类中定义一个名字属性的变量,并且通过构造方法初始化 管理员 或 用户 的名称:public abstract class User {     public String name; //名字     public User(String name) {        this.name = name; //初始化名称    }}2.2.2  Administrator(管理员类)因为我们的管理员类继承了User类,所以需要在构造方法中调用父类的构造方法:public class Administrator extends User{    public Administrator(String name) {        super(name);//通过父类初始化管理员名称    }}2.2.3 Visitor(用户类)同理用户类也继承了User类,所以也需要构造方法调用父类的构造方法:public class Visitor extends User{    public Visitor(String name) {        super(name); //通过父类初始化用户名称    }}那么书籍的属性和操作者的基本框架我们已经实现了,接下来该实现一下图书管理系统的功能了~2.3 图书管理系统操作菜单管理员菜单1.查找图书2.新增图书3.删除图书4.显示图书0.退出系统用户菜单1.查找图书2.借阅图书3.归还图书0.退出系统接下来,我们就针对这些功能创建一个包,实现这些功能。2.4 operation(操作包)在这个包中我们去实现上述描述的相关操作,接着我们创建一个接口来实现多态,从而降低代码的复杂度operate接口:public interface Operate {  //服务接口    void work(BookList books);    //功能的实现我们都要利用到书架,所以这里参数给定一个书架对象}接下来我们创建实现各个功能的类: Addbook类:(新增书籍)public class Addbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("增加书籍...");    }}Borrowbook类:(借阅书籍)public class Borrowbook implements Operate {    @Override    public void work(BookList books) {        System.out.println("借阅书籍....");    }} Deletebook类:(删除书籍)public class Deletebook implements Operate{    @Override    public void work(BookList books) {        System.out.println("删除书籍...");    }}Exitbook类:(退出系统)public class Exitbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("退出系统.....");    }}Findbook类:(查阅书籍)public class Findbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("查找书籍....");    ]}Returnbook类:(归还书籍)public class Retuenbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("归还书籍....");Showbook类:(显示书籍)public class Showbook implements Operate {    @Override    public void work(BookList books) {        System.out.println("显示书籍....");    }}接下来就要慢慢的去将每个类的功能具体的实现,并且将每个类联系起来运作图书管理系统。既然知道了管理员和用户的菜单后,我们就分别给管理员和用户添加一下菜单:在Administrator类添加管理员菜单:public void menu() {        System.out.println("欢迎 "+this.name+" 管理员来到图书管理系统");        System.out.println("<<**************管理员操作菜单******************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 新增图书 ");        System.out.println("3. 删除图书 ");        System.out.println("4. 显示图书");        System.out.println("0. 退出系统 ");        System.out.println("<<******************************************>>");}在Visitor类中添加用户菜单:public void menu(){        System.out.println("欢迎 "+this.name+" 读者来到图书管理系统");        System.out.println("<<***************游客读者菜单*************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 借阅图书 ");        System.out.println("3. 归还图书 ");        System.out.println("0. 退出系统 ");        System.out.println("<<**************************************>>");} 那么我们现在创建一个Main类,在类中写上我们的main方法实现一下基本逻辑:​public class Main {    public static User login(){   //登录系统过程        Scanner scanner=new Scanner(System.in);        System.out.println("请输入你的名字:");        String name = scanner.nextLine();         System.out.println("请输入你要登入的帐号:1.管理员登录 -----> 2.游客登录 ----->");        int choice=scanner.nextInt();        if(choice==1){            return new Administrator(name); //如果选择了1,就实例化一个管理员对象,并返回        }else{            return new Visitor(name); //如果选择了2,就实例化一个用户对象,并返回        }        //此时的返回值我们不能确定返回的是管理员对象还是用户对象,所以这里用向上转型返回User类型的对象    }      public static void main(String[] args) {            User user=login(); //进入登录系统            user.menu(); //打印菜单    }} ​但是此时login方法的返回对象我们使用User类型的对象进行接收的,此时我们是不能使用user.menu()的,那么该怎么办呢?我们之前将User抽象出来作为管理员类和用户类的父类,那么此时我们在User类中写一个menu()方法就可以实现多态,打印菜单啦~public abstract class User {    public String name; //用户名称     public User(String name) {        this.name = name; //初始化用户名称    }    public abstract int menu(); //抽象方法实现多态,打印菜单}那么此时只需要将子类做出相应的改动,改变成重写的形式就可以了: 这就实现了打印菜单的功能,但是光打印选择不了功能可不行呀,所以我们要在菜单中加上一个选择功能:  @Override    public int  menu() {        System.out.println("欢迎 "+this.name+" 管理员来到图书管理系统");        System.out.println("<<**************管理员操作菜单******************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 新增图书 ");        System.out.println("3. 删除图书 ");        System.out.println("4. 显示图书");        System.out.println("0. 退出系统 ");        System.out.println("<<******************************************>>");        //选择功能        Scanner scanner=new Scanner(System.in);        System.out.println("请选择您需要的服务:-->>");        int choice=scanner.nextInt();        return choice;    }通过输入标号来选择我们想要的服务,那么我们应该返回这个标号,所以menu()方法的返回值也要改成 int 类型,Visitor类的menu()方法也是同理。那么能够选择服务后,我们应该根据对象(管理员或用户)选择该调用哪一个菜单方法:Administrator(管理员类):public Administrator(String name) {        super(name);//通过父类初始化管理员名称        this.operate=new Operate[]{  //创建 管理员账户提供的服务                new Exitbook(),                new Findbook(),                new Addbook(),                new Deletebook(),                new Showbook()        };    }Visitor(用户类):public class Visitor extends User{    public Visitor(String name) {        super(name); //通过父类初始化用户名称        this.operate=new Operate[]{   //创建 游客账户提供的服务                new Exitbook(),                new Findbook(),                new Borrowbook(),                new Retuenbook()        };    } 那么当我们分别在它们的构造方法中创建一个Operate类型的数组后,里面存放Operate接口实现的方法,那么在new一个管理员对象或者用户对象时,系统就会为这个数组分配内存: 那么我们需要在User类中加上这么一段代码利用动态绑定调用管理员菜单中的方法还是调用用户菜单中的方法:    public Operate []operate;  //创建服务数组,子类通过数组下标调用 各个服务     public void Dooperate(int choice,BookList books){        this.operate[choice].work(books);  //调用 游客/管理员 所选择的服务    }this.operate[ choice ]就是我们new的那个对象中构造方法中下标为 choice 的那个类,而后面的 .work(books)就是调用的对应类的work方法。那么现在我们在main方法中打印菜单的时候就需要有个变量来接收选择的服务啦,再通过new对应的对象调用上面写的 Dooperate方法调用所选择的服务。public static void main(String[] args) {            BookList bookList=new BookList(); //实例化书架            User user=login(); //进入登录系统            while(true) {                int chioce = user.menu(); //接收选择服务的选项                user.Dooperate(chioce, bookList); //调用 游客/管理员 提供的服务            }    }而为了实现用户输入0时才退出系统,所以这里的while循环我们设置为死循环。此时基本框架就搭建完成啦~我们来看看测试效果:那么我们来画图分析一下运行的过程:1. 通过主函数调用login方法: 2. 通过login方法返回的对象调用该对象类中的menu()方法 3. 最后通过调用user.Dooperate方法一步一步实现所选择的服务 三. 实现服务 3.1 显示所有书籍那么在实现显示书籍功能之前,我们应该在Booklist类中写一个方法让Showbook类中的work方法能够通过数组下标打印书籍的信息:    public Book getbook(int pos){        return this.books[pos];  //返回书架中 第pos本书    }接下来我们就可以实现显示书籍这个服务啦~在Showbook类中实现: public class Showbook implements Operate {    @Override    public void work(BookList books) {        System.out.println("显示书籍....");        int count=books.getUsedbooks();        for(int i=0;i<count;i++) {            //通过循环遍历数组;调用books.getbook(i)方法获取指定的书籍对象            System.out.println(books.getbook(i).toString());        }        System.out.println();    }}3.2 查找书籍查找书籍的过程与显示所有书籍相似,也是通过循环遍历数组,利用equal()方法进行对比,实现服务。在Findbook类中实现:public class Findbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("查找书籍....");        Scanner scanner=new Scanner(System.in);        System.out.print("请输入要查找书籍的名字:-->");        String name=scanner.nextLine();        int count=books.getUsedbooks();        for(int i=0;i<count;i++)        {            Book book=books.getbook(i);            if(book.getName().equals(name)){                System.out.println("找到了,书本内容如下:");                System.out.println(book);                System.out.println();                return;            }        }        System.out.println("很抱歉,没有查找到你要查找的书籍");        System.out.println("后续会尽快联系管理员添加~");        System.out.println();    }} 3.3 退出系统在Exitbook类中实现:public class Exitbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("退出系统.....");        int count=books.getUsedbooks();        for(int i=0;i<count;i++)        {           books.setBooks(i,null); //        }        books.setUsedbooks(0); //将有效书籍数量 制为空        System.exit(0);    }}书架中存放的书籍都是对象,那么在退出系统之前我们应该将书籍对象都进行回收,避免发生内存泄漏,并且将书架中的书本数量变成0,最后在结尾加上 System.exit(0); 就可以结束程序,退出系统了。3.4 增加书籍在增加书籍之前,我们应该先判断这个书籍是否已经在书架中存放,如果书架中没有那么就添加到书架中在Addbook类中实现:public class Addbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("增加书籍...");        int count=books.getUsedbooks();        Scanner scanner=new Scanner(System.in);        System.out.print("请输入新添加的书名:--->");        String name=scanner.nextLine();         System.out.print("请输入新添加书籍的作者:--->");        String author=scanner.nextLine();         System.out.print("请输入书籍的价格:--->");        int price=scanner.nextInt();        scanner.nextLine();        System.out.print("请输入书籍的类型:--->");        String type=scanner.nextLine();         for(int i=0;i<count;i++) {           if(books.getbook(i).getName().equals(name)){               System.out.println("该书籍已经存在了,不需要添加-----");               System.out.println();               return ;           }        }        Book book=new Book(name,author,price,type);        books.setBooks(count,book);        System.out.println("添加成功---");        books.setUsedbooks(count+1);        System.out.println();    }}那么在Booklist类中我们就要提供一个方法用来新增书籍:    public void setBooks(int pos,Book book){        this.books[pos]=book;  //更改书架中的书    }3.5 删除书籍在删除书籍中我们有两个点需要注意:当我们删除了某本书籍后,需要将该书籍后面的书籍往前移动,并且将移动完最后的那个空间进行回收,并且更改书架中书籍的数量在Deletebook类中实现:public class Deletebook implements Operate{    @Override    public void work(BookList books) {        System.out.println("删除书籍...");        System.out.print("请输入你要删除的书籍:--->");        Scanner scanner=new Scanner(System.in);        String name =scanner.nextLine();        int count=books.getUsedbooks();        int index=-1;        boolean flag=false;        for (int i = 0; i <count ; i++) {            if(books.getbook(i).getName().equals(name)){               index=i;               flag=true;               break;            }        }        if(flag==false) {            System.out.println("没有找到你要删除的书籍-----");            System.out.println();            return ;        }        if (books.getbook(index).isIsborrow()==true){            System.out.println("该书本已经被借出,暂时不能进行操作-----");            System.out.println();            return;        }else{            for(int j=index;j<count-1;j++)            {                books.setBooks(j,books.getbook(j+1));            }            System.out.println("删除成功------->");            books.setUsedbooks(count-1);            books.setBooks(count-1,null);        }    }}那么在删除书籍的时候为了实现将后面的书籍往前面移动,我们需要在Booklist类中调用setBooks方法来实现这个过程:    public void setBooks(int pos,Book book){        this.books[pos]=book;  //更改书架中的书    }3.6 借阅书籍借阅书籍的时候我们应该判断该书籍是否在书架上存在,如果存在再判断该书籍是否被借出,如果没有被借出,那么就可以进行借阅在Borrowbook类中实现:ublic class Borrowbook implements Operate {    @Override    public void work(BookList books) {        System.out.println("借阅书籍....");        System.out.print("请输入你要借阅的图书:-->");        Scanner scanner = new Scanner(System.in);        String name = scanner.nextLine();         int count = books.getUsedbooks();        for (int i = 0; i < count; i++) {            if (books.getbook(i).getName().equals(name)) {                if (books.getbook(i).isIsborrow() == false) {                    books.getbook(i).setIsborrow(true);                    System.out.println("借阅成功---");                    System.out.println(books.getbook(i));                    System.out.println();                    return;                }else{                    System.out.println("这本书已经被借出,请等待读者归还后再进行借阅---");                    System.out.println();                    return ;                }            }        }        System.out.println("查找不到你要借阅的书籍,后续会联系管理员尽快上架!!!");        System.out.println();    }}这个时候我们需要在Book类中提供一个方法,返回当前书籍是否被借出的状态,并且还要提供一个方法改变书籍是否被借出的状态,在Book类中实现:    public boolean isIsborrow() {        return isborrow;    }    public void setIsborrow(boolean isborrow) {        this.isborrow = isborrow;    } 3.7 归还书籍在归还之前我们需要判断一下这本书是否在书架上存在过,如果存在再进行判断这本书是否有被借出,如果有被借出则可以归还在Returnbook类中实现: public class Retuenbook implements Operate{    @Override    public void work(BookList books) {        System.out.println("归还书籍....");        System.out.print("请输入你要归还的图书:-->");        Scanner scanner = new Scanner(System.in);        String name = scanner.nextLine();         int count = books.getUsedbooks();        for (int i = 0; i < count; i++) {            if (books.getbook(i).getName().equals(name)) {                if (books.getbook(i).isIsborrow() == true) {                    books.getbook(i).setIsborrow(false);                    System.out.println("归还成功---欢迎再次借阅");                    System.out.println(books.getbook(i));                    System.out.println();                    return;                }else{                    System.out.println("这本书没有被借出,不需要归还---");                    System.out.println();                    return ;                }            }        }        System.out.println("查找不到你要归还的书籍,你可能是在其他图书馆借阅的书籍---");        System.out.println();    }}如果将书籍归还后我们需要将书籍是否借出的状态进行改变,在Book类中实现:public boolean isIsborrow() {        return isborrow;    }    public void setIsborrow(boolean isborrow) {        this.isborrow = isborrow;    }四. 总代码Main类:import book.BookList;import user.Administrator;import user.User;import user.VipPerson;import user.Visitor;import java.util.Scanner; public class Main {    public static User login(){   //登录系统过程        Scanner scanner=new Scanner(System.in);        System.out.println("请输入你的名字:");        String name = scanner.nextLine();         System.out.println("请输入你要登入的帐号:1.管理员登录 -----> 2.游客登录 ----->");        int choice=scanner.nextInt();        if(choice==1){            return new Administrator(name); //如果选择了1,就创建一个管理员对象,并返回        }else if(choice==2){            return new Visitor(name); //如果选择了2,就创建一个游客对象,并返回        }else{            return new VipPerson(name);        }        //此时的返回值我们不能确定返回的是管理员对象还是用户对象,所以这里用向上转型返回User类型的对象    }    public static void main(String[] args) {            BookList bookList=new BookList(); //实例化书架            User user=login(); //进入登录系统            while(true) {                int chioce = user.menu(); //接收选择服务的选项                user.Dooperate(chioce, bookList); //调用 游客/管理员 提供的服务            }    }}User类:package user;import book.BookList;import operation.Operate; public abstract class User {    public String name; //用户名称     public Operate []operate;  //创建服务数组,子类通过数组下标调用 各个服务    public User(String name) {        this.name = name; //初始化用户名称    }    public abstract int menu(); //服务菜单    public void Dooperate(int choice,BookList books){        this.operate[choice].work(books);  //调用 游客/管理员 所选择的服务    }} Administrator类:package user;import operation.*;import java.util.Scanner;public class Administrator extends User{    public Administrator(String name) {        super(name);//通过父类初始化管理员名称        this.operate=new Operate[]{  //创建 管理员账户提供的服务                new Exitbook(),                new Findbook(),                new Addbook(),                new Deletebook(),                new Showbook()        };    }    @Override    public int  menu() {        System.out.println("欢迎 "+this.name+" 管理员来到图书管理系统");        System.out.println("<<**************管理员操作菜单******************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 新增图书 ");        System.out.println("3. 删除图书 ");        System.out.println("4. 显示图书");        System.out.println("0. 退出系统 ");        System.out.println("<<******************************************>>");        //选择功能        Scanner scanner=new Scanner(System.in);        System.out.println("请选择您需要的服务:-->>");        int choice=scanner.nextInt();        return choice;    }}Visitor类:package user;import operation.*;import java.util.Scanner;public class Visitor extends User{    public Visitor(String name) {        super(name); //通过父类初始化用户名称        this.operate=new Operate[]{   //创建 游客账户提供的服务                new Exitbook(),                new Findbook(),                new Borrowbook(),                new Retuenbook()        };    }     public int menu(){        System.out.println("欢迎 "+this.name+" 读者来到图书管理系统");        System.out.println("<<***************游客读者菜单*************>>");        System.out.println("1. 查找图书 ");        System.out.println("2. 借阅图书 ");        System.out.println("3. 归还图书 ");        System.out.println("0. 退出系统 ");        System.out.println("<<**************************************>>");        Scanner scanner=new Scanner(System.in);        System.out.println("请选择您需要的服务:-->>");        int choice=scanner.nextInt();        return choice;    }}Book类:package book;public class Book {     //书的属性    private String name; //书名    private String author; //作者    private  int price;  //价格    private String type; //书籍类型    private boolean isborrow; //是否被借出    public Book(String name, String author, int price, String type) {        //初始化书的属性        this.name = name;        this.author = author;        this.price = price;        this.type = type;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getAuthor() {        return author;    }    public void setAuthor(String author) {        this.author = author;    }    public int getPrice() {        return price;    }    public void setPrice(int price) {        this.price = price;    }    public String getType() {        return type;    }    public void setType(String type) {        this.type = type;    }    public boolean isIsborrow() {        return isborrow;    }    public void setIsborrow(boolean isborrow) {        this.isborrow = isborrow;    }     @Override    public String toString() {        return "Book{" +                "书名='" + name + '\'' +                ", 作者='" + author + '\'' +                ", 价格=" + price +                ", 类型='" + type + '\'' +                (isborrow==true?" ,已经借出":" ,未被借出") +                '}';    }}Booklist类:package book;public class BookList {    private Book [] books;//表示书架可以存放10本书    private int usedbooks;//表示books数组中存放了几本书     public BookList() {        this.books = new Book [10];  //默认创建存放10本书大小的数组        this.books[0]=new Book("三国演义","罗贯中",20,"四大名著");        this.books[1]=new Book("西游记","吴承恩",22,"四大名著");        this.books[2]=new Book("红楼梦","曹雪芹",24,"四大名著");        this.books[3]=new Book("水浒传","施耐庵",26,"四大名著");        this.usedbooks=4;//初始化有效书本数量为4本    }    public int getUsedbooks() {        return usedbooks;//返回有效书本数量    }    public void setUsedbooks(int usedbooks) {        this.usedbooks = usedbooks;//更改有效书本数量    }    public Book getbook(int pos){        return this.books[pos];  //返回书架中 第pos本书    }    public void setBooks(int pos,Book book){        this.books[pos]=book;  //更改书架中的书    }} 那么operation这个包中各个类的代码都在 实现服务 那里写啦,这里就不在重新上传了~结语以上就是利用我们过去的所学知识实现的图书管理系统的基本逻辑构造,在此感谢大家的观看!!————————————————                  原文链接:https://blog.csdn.net/2402_86304740/article/details/143254119
  • [技术干货] Java DL4J 2024年度技术总结
    引言在当今数字化浪潮中,深度学习作为人工智能领域的核心驱动力,正以前所未有的速度改变着我们的生活和工作方式。从图像识别到自然语言处理,从医疗诊断到金融预测,深度学习的应用场景无处不在,展现出巨大的潜力和价值。Java作为一门广泛应用于企业级开发的编程语言,以其稳定性、可移植性和丰富的类库资源,在软件开发领域占据着重要地位。然而,传统的Java开发在面对深度学习复杂的模型构建和大规模数据处理时,往往显得力不从心。DL4J(Deeplearning4j)的出现,为Java开发者打开了一扇通往深度学习世界的大门。DL4J是一个专为Java和Scala设计的深度学习框架,它将深度学习的强大功能与Java的企业级特性完美结合。通过DL4J,Java开发者无需深入掌握复杂的底层数学原理和编程语言,就能够利用Java的生态优势,快速搭建和训练深度学习模型。在过去的一年里,我深入研究和实践了Java DL4J深度学习,积累了丰富的经验和见解。在本文,我们将对这一年在Java DL4J深度学习领域的技术探索进行全面总结,让为我们一起来回顾DL4J的整个概况吧!一、Java DL4J深度学习概述1.1 DL4J框架简介DL4J是基于 Java 和 Scala的分布式深度学习库,它构建在ND4J(一个用于 Java 和 Scala 的数值运算库)之上,提供了丰富的神经网络模型和工具,支持多种深度学习任务,如卷积神经网络(CNN)用于图像识别、循环神经网络(RNN)及其变体(如LSTM、GRU)用于处理序列数据等。DL4J的设计目标是让Java开发者能够像使用传统Java库一样轻松地进行深度学习开发,同时保持高性能和可扩展性。1.2 与其他深度学习框架的比较与Python的TensorFlow和PyTorch等热门深度学习框架相比,DL4J具有独特的优势。首先,在语言层面,Java的静态类型系统和强大的企业级生态使其更适合构建大规模、高可靠性的深度学习应用,尤其在对稳定性和安全性要求较高的企业级场景中。其次,DL4J提供了与Java生态系统的无缝集成,方便与其他Java技术栈(如Spring框架、Hadoop等)结合使用,实现端到端的解决方案。然而,Python的深度学习框架由于其简洁的语法和庞大的社区支持,在快速原型开发和研究领域更为流行。DL4J(Deeplearning4j)则在生产环境部署和企业级应用开发方面展现出明显的优势。1.3 DL4J 的优势1.3.1 与 Java 生态系统的无缝集成由于 DL4J 是用 Java 编写的,它可以与现有的 Java 项目轻松集成,利用 Java 丰富的类库和工具,提高开发效率。1.3.2 分布式计算支持DL4J 支持分布式训练,能够充分利用集群计算资源,加速模型训练过程,适用于大规模数据集的深度学习任务。1.3.3 高度可定制DL4J 提供了丰富的 API,开发者可以根据具体需求灵活定制神经网络结构、优化算法和训练参数,实现个性化的深度学习模型。二、开发环境搭建2.1 安装Java JDK首先,确保系统安装了合适版本的Java JDK(Java Development Kit)。DL4J支持Java 8及以上版本。可以从Oracle官方网站或OpenJDK官网下载并安装相应的JDK。安装完成后,配置系统环境变量JAVA_HOME,指向JDK的安装目录,并将%JAVA_HOME%\bin添加到系统的PATH环境变量中,以便在命令行中能够正确识别java和javac命令。2.2 配置Maven项目Maven是Java项目中常用的构建工具,用于管理项目的依赖和构建过程。创建一个新的Maven项目,可以使用Maven的命令行工具或集成开发环境(IDE)如Eclipse、IntelliJ IDEA等。在项目的pom.xml文件中,需要引入DL4J相关的依赖。<!-- 引入DL4J(deeplearning4j)核心依赖 --><dependency>    <groupId>org.deeplearning4j</groupId>    <artifactId>deeplearning4j-core</artifactId>    <version>1.0.0-beta7</version></dependency><!-- 引入ND4J后端依赖,这里以CPU后端为例。Nd4j 是 DL4J 的底层数值运算库,为 DL4J 提供了高效的矩阵运算支持。 --><dependency>    <groupId>org.nd4j</groupId>    <artifactId>nd4j-native-platform</artifactId>    <version>1.0.0-beta7</version></dependency><!-- 引入数据加载和预处理相关依赖 --><dependency>    <groupId>org.deeplearning4j</groupId>    <artifactId>deeplearning4j-ui</artifactId>    <version>1.0.0-beta7</version></dependency><dependency>    <groupId>org.deeplearning4j</groupId>    <artifactId>deeplearning4j-datavec</artifactId>    <version>1.0.0-beta7</version></dependency>上述代码中:deeplearning4j-core是DL4J的核心库,包含了深度学习模型构建、训练和评估的基本功能。nd4j-native-platform是ND4J的本地平台实现,提供了数值计算的底层支持。这里选择了CPU版本,如果需要使用GPU加速,可以引入相应的GPU版本依赖。deeplearning4j-ui提供了可视化工具,方便监控模型训练过程。deeplearning4j-datavec用于数据加载、预处理和转换,是构建深度学习模型的重要环节。2.3 选择合适的IDE选择一个功能强大的IDE对于开发效率至关重要。IntelliJ IDEA以其丰富的Java开发功能和对Maven项目的良好支持,成为许多Java开发者的首选。在IntelliJ IDEA中,导入创建好的Maven项目,IDE会自动下载并解析pom.xml中定义的依赖。同时,IDE提供了代码自动完成、调试等功能,方便开发者编写和测试DL4J应用程序。三、深度学习基础概念3.1 神经网络神经网络是深度学习的核心概念之一,它模仿人类神经系统的结构和工作方式。一个简单的神经网络由输入层、隐藏层和输出层组成。输入层接收外部数据,隐藏层对数据进行特征提取和转换,输出层根据隐藏层的处理结果产生最终的预测或分类结果。例如,在一个手写数字识别的神经网络中,输入层接收图像的像素值,隐藏层通过一系列的神经元计算提取图像中的特征,如线条、轮廓等,输出层则根据这些特征判断图像中的数字是 0 到 9 中的哪一个。3.2 神经元与激活函数神经元是神经网络的基本计算单元,它接收多个输入信号,并通过加权求和的方式将这些输入信号组合起来,再经过激活函数的处理得到输出。激活函数的作用是为神经网络引入非线性因素,使得神经网络能够学习到复杂的非线性关系。常见的激活函数有 sigmoid 函数、ReLU(Rectified Linear Unit)函数等。sigmoid 函数将输入值映射到 0 到 1 之间,其公式为:σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}}σ(x)= 1+e −xReLU 函数则更为简单,当输入大于 0 时,输出等于输入;当输入小于等于 0 时,输出为 0,其公式为:f ( x ) = max ⁡ ( 0 , x ) f(x) = \max(0, x)f(x)=max(0,x)3.3 反向传播算法反向传播算法是神经网络训练的核心算法,它用于计算损失函数关于网络参数(权重和偏置)的梯度,以便通过梯度下降等优化算法更新参数,使得损失函数最小化。反向传播算法的基本思想是从输出层开始,根据损失函数计算输出层的误差,然后将误差反向传播到隐藏层,依次计算每个隐藏层的误差,最后根据误差计算梯度并更新参数。四、核心概念与模型构建4.1 神经网络基础神经网络是深度学习的核心概念,它由大量的神经元组成,通过模拟人类大脑的神经元结构和工作方式来处理数据。在DL4J中,神经网络的基本构建块是Layer(层)。常见的层类型包括:输入层(Input Layer):负责接收输入数据,数据以张量(Tensor)的形式传入。例如,对于图像识别任务,输入层可以接收一个三维张量,分别表示图像的高度、宽度和通道数(如RGB图像通道数为3)。// 定义输入层InputLayer inputLayer = new InputLayer.Builder()     .nIn(inputSize)     .build();上述代码中,inputSize表示输入数据的维度,通过InputLayer.Builder来配置输入层的参数并构建输入层对象。全连接层(Fully Connected Layer):也称为密集层(Dense Layer),层中的每个神经元都与前一层的所有神经元相连。它通过权重矩阵和偏置项对输入数据进行线性变换,然后通过激活函数引入非线性。// 定义全连接层DenseLayer denseLayer = new DenseLayer.Builder()     .nIn(inputSize)     .nOut(outputSize)     .activation("relu")     .build();这里,nIn表示输入维度,nOut表示输出维度,activation指定激活函数,如relu(修正线性单元)。激活函数(Activation Function):用于引入非线性,使神经网络能够学习复杂的模式。常见的激活函数有Sigmoid、Tanh、ReLU等。不同的激活函数具有不同的特性和适用场景。例如,ReLU函数在处理大规模数据时具有计算效率高、不易出现梯度消失等优点。4.2 卷积神经网络(CNN)CNN是专门为处理具有网格结构数据(如图像、音频)而设计的神经网络。它通过卷积层、池化层和全连接层的组合来自动提取数据的特征。卷积层(Convolutional Layer):使用卷积核(Filter)对输入数据进行卷积操作,提取局部特征。卷积核在输入数据上滑动,每次滑动计算卷积核与局部数据的点积,得到卷积结果。// 定义卷积层ConvolutionLayer convolutionLayer = new ConvolutionLayer.Builder()     .kernelSize(3, 3)     .stride(1, 1)     .nIn(inputChannels)     .nOut(outputChannels)     .activation("relu")     .build();其中,kernelSize指定卷积核的大小,stride表示卷积核滑动的步长,nIn是输入通道数,nOut是输出通道数。池化层(Pooling Layer):用于对卷积层的输出进行下采样,减少数据维度,同时保留主要特征。常见的池化方法有最大池化(Max Pooling)和平均池化(Average Pooling)。// 定义最大池化层SubsamplingLayer poolingLayer = new SubsamplingLayer.Builder(SubsamplingLayer.PoolingType.MAX)     .kernelSize(2, 2)     .stride(2, 2)     .build();这里选择了最大池化,kernelSize和stride的含义与卷积层类似。4.3 循环神经网络(RNN)及其变体RNN适用于处理序列数据,如时间序列、文本等。它通过引入反馈机制,能够记住过去的信息并用于当前的决策。然而,传统RNN存在梯度消失和梯度爆炸的问题,限制了其在长序列数据处理中的应用。为了解决这些问题,出现了一些RNN的变体,如长短期记忆网络(LSTM)和门控循环单元(GRU)。LSTM:LSTM通过引入记忆单元(Cell)和多个门控机制(输入门、遗忘门、输出门)来有效地控制信息的流动,从而能够处理长序列数据。// 定义LSTM层LSTM.Builder lstmBuilder = new LSTM.Builder()     .nIn(inputSize)     .nOut(outputSize)     .build();GRU:GRU是LSTM的简化版本,它将输入门和遗忘门合并为一个更新门,减少了模型的参数数量,同时在性能上与LSTM相当。// 定义GRU层GRU.Builder gruBuilder = new GRU.Builder()     .nIn(inputSize)     .nOut(outputSize)     .build();五、数据处理与加载5.1 数据预处理在将数据输入到深度学习模型之前,需要进行预处理,以提高模型的训练效果和效率。常见的数据预处理步骤包括:数据归一化(Normalization):将数据的特征值缩放到一定范围内,如[0, 1]或[-1, 1]。这有助于加速模型的收敛和提高泛化能力。在DL4J中,可以使用DataNormalization接口及其实现类进行数据归一化。// 使用MinMaxScaler进行数据归一化MinMaxScaler scaler = new MinMaxScaler(0, 1);scaler.fit(data);INDArray normalizedData = scaler.transform(data);这里,data是输入的数据集,MinMaxScaler将数据缩放到[0, 1]区间。数据标准化(Standardization):将数据的特征值转换为均值为0,标准差为1的分布。这可以通过计算数据的均值和标准差,并对每个特征值进行相应的变换来实现。// 使用StandardScaler进行数据标准化StandardScaler scaler = new StandardScaler();scaler.fit(data);INDArray standardizedData = scaler.transform(data);5.2 数据加载DL4J提供了DataVec库来加载和处理各种格式的数据。对于常见的数据集格式,如CSV、图像文件等,都有相应的加载器。加载CSV数据:可以使用CSVRecordReader来读取CSV文件中的数据。// 创建CSVRecordReaderCSVRecordReader recordReader = new CSVRecordReader();recordReader.initialize(new FileSplit(new File("data.csv")));// 创建DataSetIteratorDataSetIterator iterator = new CSVDataSetIterator(recordReader, batchSize, labelIndex, numClasses);这里,batchSize表示每次加载的数据批次大小,labelIndex是标签所在的列索引,numClasses是分类问题中的类别数。加载图像数据:对于图像数据,可以使用ImageLoader和ImageRecordReader来加载和预处理图像。// 创建ImageRecordReaderImageRecordReader recordReader = new ImageRecordReader(height, width, channels, new LabelsSource() {    @Override    public List<String> getLabels() {        return Arrays.asList("class1", "class2", "class3");    }});recordReader.initialize(new FileSplit(new File("images")));// 创建DataSetIteratorDataSetIterator iterator = new ImageDataSetIterator(recordReader, batchSize, 1, numClasses);其中,height、width和channels分别表示图像的高度、宽度和通道数。六、模型训练与优化6.1 定义损失函数损失函数(Loss Function)用于衡量模型预测结果与真实标签之间的差异,是模型训练的目标函数。常见的损失函数有:均方误差(Mean Squared Error,MSE):适用于回归问题,计算预测值与真实值之间误差的平方的平均值。// 使用均方误差损失函数LossFunction lossFunction = LossFunction.MSE;12交叉熵损失(Cross Entropy Loss):常用于分类问题,衡量两个概率分布之间的差异。在多分类问题中,通常使用Softmax交叉熵损失。// 使用Softmax交叉熵损失函数LossFunction lossFunction = LossFunction.NEGATIVELOGLIKELIHOOD;6.2 选择优化器优化器用于调整模型的参数,以最小化损失函数。DL4J提供了多种优化器,如随机梯度下降(SGD)、Adagrad、Adadelta、Adam等。随机梯度下降(SGD):最基本的优化器,每次迭代使用一个小批量的数据计算梯度并更新参数。// 使用随机梯度下降优化器Optimizer optimizer = new SGD.Builder()     .learningRate(0.01)     .build();这里,learningRate是学习率,控制每次参数更新的步长。Adam优化器:结合了Adagrad和Adadelta的优点,自适应调整学习率,在许多情况下表现良好。// 使用Adam优化器Optimizer optimizer = new Adam.Builder()     .learningRate(0.001)     .build();6.3 模型训练在定义好模型结构、损失函数和优化器后,就可以进行模型训练了。训练过程通常包括多个epoch(轮次),在每个epoch中,模型对训练数据进行多次迭代,不断调整参数以降低损失。// 创建MultiLayerNetwork模型MultiLayerNetwork model = new MultiLayerNetwork(new NeuralNetConfiguration.Builder()     .list()     .layer(0, inputLayer)     .layer(1, denseLayer)     .layer(2, outputLayer)     .build());model.init();// 定义训练配置TrainingConfig trainingConfig = new TrainingConfig.Builder()     .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)     .lossFunction(lossFunction)     .optimizer(optimizer)     .build();// 创建Trainer对象进行训练Trainer trainer = model.trainer(trainingConfig);for (int epoch = 0; epoch < numEpochs; epoch++) {    trainer.fit(trainingData);}上述代码中,MultiLayerNetwork是DL4J中用于构建多层神经网络的类,TrainingConfig配置了训练的相关参数,Trainer负责执行训练过程。七、模型评估与调优7.1 模型评估指标在训练完成后,需要对模型的性能进行评估。常见的评估指标有:准确率(Accuracy):分类问题中,预测正确的样本数占总样本数的比例。// 计算准确率Evaluation evaluation = new Evaluation(numClasses);INDArray output = model.output(testData.getFeatures());evaluation.eval(testData.getLabels(), output);System.out.println(evaluation.stats());这里,Evaluation类用于计算各种评估指标,testData是测试数据集。召回率(Recall):在分类问题中,召回率衡量模型正确预测出的正例占所有正例的比例。F1值(F1-Score):F1值是准确率和召回率的调和平均数,综合反映了模型的性能。7.2 超参数调优除了上述方法,还有一些高级的超参数调优技巧。例如,学习率调度(Learning Rate Scheduling)是一种动态调整学习率的策略。在训练初期,较大的学习率有助于模型快速收敛到一个较好的解空间;而在训练后期,较小的学习率可以防止模型在最优解附近振荡,从而提高模型的精度。在 DL4J 中,可以使用 LearningRatePolicy 来实现不同的学习率调度策略。例如,StepDecay 策略会在指定的步数后按一定比例降低学习率:// 每 1000 步将学习率降低为原来的 0.1 倍LearningRatePolicy learningRatePolicy = new StepDecay(1000, 0.1);MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()     .learningRate(0.01)     .learningRatePolicy(learningRatePolicy)      // 其他配置参数     .build();随机搜索和网格搜索虽然有效,但在高维超参数空间中效率较低。而模拟退火(Simulated Annealing)算法则提供了一种在超参数空间中更智能的搜索方式。它基于物理退火过程的思想,在搜索过程中以一定概率接受较差的解,从而避免陷入局部最优。虽然在 DL4J 中没有直接的内置实现,但可以通过自定义搜索算法来结合 DL4J 使用。7.3 模型监控与早期停止为了实时监控模型的训练过程,DL4J 提供了丰富的回调函数(Callback)机制。例如,IterationListener 接口可以用于在每次迭代结束时执行特定的操作,如记录损失值和准确率:public class MyIterationListener implements IterationListener {    @Override    public void iterationDone(IterationEvent iterationEvent) {        int iteration = iterationEvent.getIteration();        double loss = iterationEvent.getNet().calculateScore();        System.out.println("Iteration " + iteration + ": Loss = " + loss);    }}// 在训练时添加监听器MultiLayerNetwork network = new MultiLayerNetwork(conf);network.init();network.setListeners(new MyIterationListener());network.fit(trainingData);早期停止机制可以通过 EpochListener 来实现。我们可以记录验证集上的性能,并在性能不再提升时停止训练:public class EarlyStoppingListener implements EpochListener {    private int noImprovementCount = 0;    private int patience = 10;    private double bestValidationScore = Double.MAX_VALUE;    @Override    public void onEpochEnd(EpochEvent epochEvent) {        double validationScore = epochEvent.getNet().calculateScore(validationData);        if (validationScore < bestValidationScore) {            bestValidationScore = validationScore;            noImprovementCount = 0;        } else {            noImprovementCount++;            if (noImprovementCount >= patience) {                System.out.println("Early stopping triggered.");                epochEvent.getNet().setListeners(new ArrayList<>()); // 停止训练            }        }    }}// 添加早期停止监听器network.setListeners(new EarlyStoppingListener());network.fit(trainingData);八、模型部署与集成8.1 模型部署到生产环境将训练好的 DL4J 模型部署到生产环境,首先要考虑模型的序列化和反序列化。DL4J 支持将 MultiLayerNetwork 模型保存为二进制文件,以便在不同环境中加载使用。// 保存模型MultiLayerNetwork model = // 训练好的模型try (OutputStream os = new FileOutputStream("model.zip")) {    ModelSerializer.writeModel(model, os, true);} catch (IOException e) {    e.printStackTrace();}在生产环境中加载模型进行预测:// 加载模型MultiLayerNetwork loadedModel;try (InputStream is = new FileInputStream("model.zip")) {    loadedModel = ModelSerializer.restoreMultiLayerNetwork(is);} catch (IOException e) {    e.printStackTrace();    return;}// 进行预测INDArray input = Nd4j.create(new double[]{/* 输入数据 */});INDArray output = loadedModel.output(input);对于生产环境中的实时预测服务,我们可以使用 Java 的 Servlet 或更现代化的框架如 Spring Boot 来构建 RESTful API。以下是一个简单的Spring Boot 示例,用于接收输入数据并返回模型预测结果:import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RestController;import org.deeplearning4j.nn.multilayer.MultiLayerNetwork;import org.nd4j.linalg.api.ndarray.INDArray;import org.nd4j.linalg.factory.Nd4j;@SpringBootApplication@RestControllerpublic class ModelDeploymentApplication {    private static MultiLayerNetwork loadedModel;    static {        try (InputStream is = new FileInputStream("model.zip")) {            loadedModel = ModelSerializer.restoreMultiLayerNetwork(is);        } catch (IOException e) {            e.printStackTrace();        }    }    @PostMapping("/predict")    public double[] predict(@RequestBody double[] inputData) {        INDArray input = Nd4j.create(inputData);        INDArray output = loadedModel.output(input);        return output.toDoubleVector();    }    public static void main(String[] args) {        SpringApplication.run(ModelDeploymentApplication.class, args);    }}8.2 与其他系统的集成在实际项目中,深度学习模型通常需要与其他系统进行集成。例如,与企业的数据库系统集成,以获取训练数据或存储预测结果。假设我们使用 MySQL 数据库,使用 JDBC 来读取数据用于模型训练:import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.Statement;public class DatabaseReader {    public static INDArray readDataFromDatabase() {        try {            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/your_database", "username", "password");            Statement statement = connection.createStatement();            ResultSet resultSet = statement.executeQuery("SELECT * FROM your_table");            int rows = 0;            while (resultSet.next()) {                rows++;            }            resultSet.beforeFirst();            int cols = resultSet.getMetaData().getColumnCount();            INDArray data = Nd4j.create(rows, cols);            int rowIndex = 0;            while (resultSet.next()) {                for (int colIndex = 1; colIndex <= cols; colIndex++) {                    data.putScalar(new int[]{rowIndex, colIndex - 1}, resultSet.getDouble(colIndex));                }                rowIndex++;            }            connection.close();            return data;        } catch (Exception e) {            e.printStackTrace();            return null;        }    }}将预测结果存储回数据库:import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;public class DatabaseWriter {    public static void writePredictionsToDatabase(double[] predictions) {        try {            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/your_database", "username", "password");            String sql = "INSERT INTO prediction_results (prediction) VALUES (?)";            PreparedStatement preparedStatement = connection.prepareStatement(sql);            for (double prediction : predictions) {                preparedStatement.setDouble(1, prediction);                preparedStatement.executeUpdate();            }            connection.close();        } catch (Exception e) {            e.printStackTrace();        }    }}九、年度总结与展望在过去一年对 Java DL4J 深度学习的实践探索中,我们经历了从理论学习到实际项目落地的完整过程。从最初搭建简单的神经网络模型,到通过不断优化和调优构建复杂且高效的深度学习架构,每一步都积累了宝贵的经验。在技术实现方面,我们熟练掌握了 DL4J 的核心 API,能够根据不同的业务需求灵活构建、训练和评估模型。通过模型评估与调优策略,我们显著提升了模型的性能和泛化能力,使其在面对各种实际数据时都能表现出色。然而,实践过程并非一帆风顺。在处理大规模数据时,内存管理和计算资源的优化成为了关键挑战。通过采用分布式计算框架和数据预处理技术,我们有效地缓解了这些问题,但仍需不断探索更高效的解决方案。深度学习领域的快速发展为我们提供了广阔的创新空间。我们计划进一步探索 DL4J 在新兴领域的应用,如强化学习与深度学习的结合,以实现更智能的决策系统。同时,随着硬件技术的持续进步,我们将致力于优化模型在新型硬件设备上的运行效率,充分发挥 GPU、TPU 等加速设备的潜力。此外,模型的可解释性和安全性也将成为重要的研究方向。在实际应用中,尤其是在医疗、金融等关键领域,理解模型的决策过程以及确保数据和模型的安全性至关重要。我们将积极探索相关技术,如特征重要性分析、对抗攻击防御等,以提升模型的可信度和可靠性。通过持续学习和实践,我们坚信能够在 Java DL4J 深度学习领域不断取得新的突破,为解决实际问题提供更强大、更可靠的技术支持,为推动行业发展贡献自己的力量。————————————————原文链接:https://blog.csdn.net/lilinhai548/article/details/145224986
  • [技术干货] Java 大视界 -- 深度洞察 Java 大数据安全多方计算的前沿趋势与应用革新
    引言:亲爱的 Java 和 大数据爱好者们,大家好!在 Java 大数据技术的探索之路上,我们已经搭建起了一套完整且稳固的技术体系。从《Java 大视界 – 基于 Java 的大数据分布式索引技术探秘(50)》中,我们了解到分布式索引技术借助分布式哈希表等精妙设计,实现了海量数据的高效存储与毫秒级检索,为大数据应用筑牢了根基。而在《Java 大视界 – Java 与大数据流式机器学习:理论与实战(51)》里,我们踏入了实时数据处理的前沿,领略到 Java 与流式机器学习融合在金融风险实时监测、工业物联网设备故障预警等场景中的卓越效能。如今,随着数字化进程的迅猛推进,数据已然成为关键资产,数据安全的重要性愈发凸显。Java 大数据安全多方计算技术应运而生,它宛如一把钥匙,开启了跨机构、跨领域安全数据合作的大门,在严守数据隐私的同时深度挖掘数据价值,为大数据时代注入新的活力与机遇。 正文:一、安全多方计算技术基础1.1 安全多方计算的概念与原理安全多方计算(Secure Multi-Party Computation,MPC)作为现代密码学领域的重要成果,其核心在于允许多个参与方在不暴露各自私有数据的情况下,协同完成既定计算任务。这一过程依赖于一系列精妙的密码学协议。以混淆电路(Garbled Circuit)协议为例,它通过对电路进行加密混淆,使得参与方在不知晓其他方输入的情况下完成计算。假设参与方 A 持有数据 x ,参与方 B 持有数据 y ,双方希望计算函数 f(x, y) ,却不想让对方知晓自己的数据。通过混淆电路协议,A 将数据 x 加密后发送给 B,B 在不知 x 具体值的情况下,结合自身数据 y 进行计算,最终得出 f(x, y) 的结果,且 A 和 B 均无法获取对方原始数据。如图 1 所示,清晰展示了混淆电路协议的工作流程:AB使用加密算法(如AES)准备数据x并加密发送加密后的数据x准备数据y结合接收到的加密数据x与自身数据y进行计算(基于混淆电路协议,使用特定计算规则)返回计算结果f(x,y)返回错误信息alt[计算成功][计算失败]AB不经意传输(Oblivious Transfer)协议则保证接收方只能获取特定信息,而发送方无法得知接收方获取的具体内容。在医疗数据查询场景中,医院 A 拥有大量患者病历数据,患者 B 希望查询自己的病历,却不想让医院 A 知道查询的是哪一份。通过不经意传输协议,患者 B 能在不暴露查询目标的情况下获取病历信息,医院 A 也无法知晓患者 B 的查询行为。具体步骤如下表所示:步骤    描述1    发送方准备多个数据项,并利用非对称加密算法对每个数据项进行加密处理,生成密文数据集合。2    接收方生成随机选择信息,例如生成一个随机数作为索引,用于指定要获取的数据项。3    发送方根据接收方的选择信息,以一种特殊方式将加密数据发送给接收方,接收方只能利用自己的私钥解密出自己选择的数据项,而发送方无法得知接收方的选择。1.2 与传统数据处理方式的区别传统数据处理模式常采用集中式架构,将所有数据汇聚到一个中心节点进行处理。这种方式虽便于管理和计算,但存在巨大安全隐患。一旦中心节点遭受攻击,数据泄露风险极高,还可能面临数据合规性难题,如违反《通用数据保护条例》(GDPR)等法规。例如,某知名社交平台曾因数据中心被攻击,导致数亿用户个人信息泄露,引发了严重的信任危机,用户对该平台的信任度大幅下降,平台也面临着巨额的罚款和业务整改。安全多方计算采用分布式计算架构,数据分散存储在各参与方本地,仅在计算时通过加密协议以密文形式交互。整个传输和计算过程如同被层层加密的黑盒,外界难以窥探其中奥秘,极大地增强了数据隐私保护能力。为更直观呈现二者差异,以下通过详细对比表格说明:对比项    传统数据处理方式    安全多方计算数据存储位置    集中于中心节点,易成为攻击目标    分散在各参与方本地,降低整体风险,即使部分节点数据泄露,也不会影响全局数据处理方式    集中计算,依赖中心节点性能,一旦中心节点出现故障,计算任务将中断    分布式计算,各节点协同工作,利用并行处理能力,提高计算效率和容错性数据安全性    中心节点安全漏洞可能导致大规模数据泄露,数据在传输和存储过程中面临较高风险    密文传输与计算,隐私保护机制严密,采用多种加密算法和协议确保数据安全应用场景    适用于数据敏感度低、追求处理效率的简单场景,如一般性的数据分析和报表生成    主要应用于对数据隐私和合规要求极高的复杂场景,如医疗、金融、政务等领域的数据共享与分析二、Java 在安全多方计算中的技术实现2.1 基于 Java 的密码学库应用Java 凭借丰富且强大的密码学库,为安全多方计算提供了坚实技术支撑。Java Cryptography Architecture(JCA)和 Java Cryptography Extension(JCE)是其中的核心组件,涵盖加密、解密、数字签名、密钥管理等全方位密码学功能。以 AES(Advanced Encryption Standard)算法为例,它是一种对称加密算法,广泛应用于数据加密场景。以下是使用 AES 算法结合 GCM(Galois/Counter Mode)模式进行数据加密和解密的 Java 代码示例,GCM 模式不仅提供数据保密性,还具备完整性验证功能:import javax.crypto.Cipher;import javax.crypto.KeyGenerator;import javax.crypto.SecretKey;import javax.crypto.spec.GCMParameterSpec;import java.nio.charset.StandardCharsets;import java.security.SecureRandom;public class AESExample {    public static void main(String[] args) throws Exception {        // 生成256位AES密钥,密钥长度越长,安全性越高        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");        keyGenerator.init(256);        SecretKey secretKey = keyGenerator.generateKey();        // 创建加密器,使用AES/GCM/NoPadding模式        Cipher encryptCipher = Cipher.getInstance("AES/GCM/NoPadding");        // 生成12字节的初始化向量(IV),用于加密过程的随机化        byte[] iv = new byte[12];        SecureRandom secureRandom = new SecureRandom();        secureRandom.nextBytes(iv);        // 创建GCM参数规范,设置认证标签长度为128位        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);        encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);        // 待加密的敏感数据        String originalData = "sensitive data";        // 执行加密操作        byte[] encryptedData = encryptCipher.doFinal(originalData.getBytes(StandardCharsets.UTF_8));        // 创建解密器,使用相同的模式和参数        Cipher decryptCipher = Cipher.getInstance("AES/GCM/NoPadding");        decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);        // 执行解密操作        byte[] decryptedData = decryptCipher.doFinal(encryptedData);        // 将解密后的字节数组转换为字符串        String decryptedString = new String(decryptedData, StandardCharsets.UTF_8);        System.out.println("Original Data: " + originalData);        System.out.println("Decrypted Data: " + decryptedString);    }}为帮助读者更好理解代码,对每一步操作详细解释如下:生成密钥:使用KeyGenerator生成 256 位的 AES 密钥,密钥长度决定加密强度,越长越安全。128 位密钥在面对强大的暴力破解攻击时,可能在较短时间内被破解,而 256 位密钥的破解难度呈指数级增长,大大提高了数据的安全性。根据密码学研究,256 位 AES 密钥在目前的计算能力下,破解时间可能长达数百年甚至更久。创建加密器:选择AES/GCM/NoPadding模式创建加密器,同时生成 12 字节的初始化向量(IV),用于加密过程的随机化,防止相同明文加密后结果相同。IV 就像加密过程中的 “随机种子”,即使相同的明文,在不同 IV 下加密结果也不同,有效增强了加密的安全性。在实际应用中,IV 的随机性和唯一性至关重要,否则可能会被攻击者利用来破解加密数据。执行加密:将待加密的数据转换为字节数组,调用加密器的doFinal方法进行加密,得到加密后的数据。这一步是加密的核心操作,doFinal方法会根据之前设置的密钥、模式和参数,对数据进行加密处理。在加密过程中,数据会被分成多个块进行处理,每个块都会经过复杂的加密运算,最终生成密文。创建解密器:使用相同的模式和参数创建解密器,确保能够正确解密。解密器的配置必须与加密器一致,才能准确还原原始数据。如果模式或参数不一致,解密将无法成功,可能会得到错误的结果或无法解密。执行解密:将加密后的数据传入解密器,得到解密后的字节数组,再转换为字符串。经过解密器处理后,加密数据被还原为原始数据,以字符串形式呈现,方便查看和使用。在解密过程中,解密器会按照加密时的逆过程对密文进行处理,将其还原为原始的明文数据。2.2 分布式计算框架与安全多方计算的融合在大数据时代,数据规模和计算复杂度呈指数级增长,分布式计算框架成为处理海量数据的关键工具。Apache Spark 作为业界领先的分布式计算框架,与安全多方计算的融合为大规模数据的安全处理开辟了新路径。下面通过一个更详细的代码示例,展示如何在 Spark 中初步实现安全多方计算的逻辑。假设我们有两个参与方的数据,分别存储在不同的 RDD 中,我们希望在不暴露原始数据的情况下计算它们的交集(简化的安全多方计算场景)。这里使用 Paillier 同态加密算法(基于 Java 实现的简单版本)来对数据进行加密处理,以保证数据安全传输和计算。首先,引入相关依赖(假设使用 Maven 管理项目):<dependencies>    <dependency>        <groupId>org.bouncycastle</groupId>        <artifactId>bcprov-jdk15on</artifactId>        <version>1.68</version>    </dependency>    <dependency>        <groupId>org.apache.spark</groupId>        <artifactId>spark-core_2.12</artifactId>        <version>3.3.1</version>    </dependency></dependencies>然后是 Java 代码实现:import org.apache.spark.SparkConf;import org.apache.spark.api.java.JavaRDD;import org.apache.spark.api.java.JavaSparkContext;import org.bouncycastle.crypto.AsymmetricCipherKeyPair;import org.bouncycastle.crypto.generators.PaillierKeyGenerator;import org.bouncycastle.crypto.params.PaillierKeyGenerationParameters;import org.bouncycastle.crypto.params.PaillierPrivateKeyParameters;import org.bouncycastle.crypto.params.PaillierPublicKeyParameters;import org.bouncycastle.math.ec.WNafUtil;import java.math.BigInteger;import java.security.SecureRandom;import java.util.Arrays;import java.util.List;public class SparkMPCExample {    public static void main(String[] args) {        SparkConf conf = new SparkConf().setAppName("SparkMPCExample").setMaster("local[*]");        JavaSparkContext sc = new JavaSparkContext(conf);        // 生成Paillier密钥对        AsymmetricCipherKeyPair keyPair = generatePaillierKeyPair();        PaillierPublicKeyParameters publicKey = (PaillierPublicKeyParameters) keyPair.getPublic();        PaillierPrivateKeyParameters privateKey = (PaillierPrivateKeyParameters) keyPair.getPrivate();        // 模拟参与方1的数据        List<Integer> data1 = Arrays.asList(1, 2, 3, 4, 5);        JavaRDD<BigInteger> encryptedData1 = sc.parallelize(data1)             .map(num -> encrypt(num, publicKey));        // 模拟参与方2的数据        List<Integer> data2 = Arrays.asList(3, 4, 5, 6, 7);        JavaRDD<BigInteger> encryptedData2 = sc.parallelize(data2)             .map(num -> encrypt(num, publicKey));        // 模拟安全多方计算求交集(这里只是简单模拟,实际会更复杂)        JavaRDD<BigInteger> intersection = encryptedData1.intersection(encryptedData2);        // 解密结果(假设只有一方有私钥可以解密)        intersection.map(encrypted -> decrypt(encrypted, privateKey))             .collect()             .forEach(System.out::println);        sc.stop();    }    private static AsymmetricCipherKeyPair generatePaillierKeyPair() {        PaillierKeyGenerator keyGen = new PaillierKeyGenerator();        keyGen.init(new PaillierKeyGenerationParameters(new SecureRandom(), 1024));        return keyGen.generateKeyPair();    }    private static BigInteger encrypt(int num, PaillierPublicKeyParameters publicKey) {        BigInteger plaintext = BigInteger.valueOf(num);        return publicKey.encrypt(plaintext);    }    private static int decrypt(BigInteger encrypted, PaillierPrivateKeyParameters privateKey) {        BigInteger decrypted = privateKey.decrypt(encrypted);        return decrypted.intValue();    }}代码解释如下:初始化 Spark:创建SparkConf和JavaSparkContext,配置应用名称和运行模式。SparkConf用于设置 Spark 应用的各种参数,如应用名称、运行模式等;JavaSparkContext是与 Spark 集群交互的入口,负责创建和管理 RDD 等分布式数据集。在实际应用中,还可以根据集群的资源情况和计算任务的需求,对 SparkConf 进行更多的配置,如设置内存分配、线程数等。生成密钥对:使用PaillierKeyGenerator生成 Paillier 密钥对,包括公钥和私钥。密钥对是安全多方计算的基础,公钥用于加密数据,私钥用于解密数据,确保数据的安全性和隐私性。在生成密钥对时,需要选择合适的密钥长度和随机数生成器,以保证密钥的安全性。数据加密:将参与方 1 和参与方 2 的数据分别转换为JavaRDD,并使用公钥对数据进行加密。JavaRDD是 Spark 中分布式弹性数据集,通过map操作将数据转换为加密后的形式,实现数据在分布式环境下的安全传输。在实际应用中,可能会遇到数据格式不统一、数据量过大等问题,需要进行相应的数据预处理和优化。计算交集:对加密后的数据调用intersection方法,模拟安全多方计算求交集。这一步是在加密数据上进行计算,确保原始数据不被泄露。在实际场景中,计算交集可能会涉及到更复杂的逻辑和算法,需要根据具体需求进行优化。解密结果:使用私钥对交集结果进行解密,并打印输出。只有拥有私钥的一方才能解密出计算结果,保证了结果的安全性和隐私性。在实际应用中,需要妥善保管私钥,防止私钥泄露导致数据安全问题。通过将安全多方计算协议无缝集成到 Spark 的分布式计算流程中,各节点能够在保护数据隐私的前提下协同工作。例如,在电商行业的联合营销分析中,多个电商平台拥有各自的用户浏览、购买、评价等数据。利用 Spark 的分布式计算能力和安全多方计算技术,这些平台可以联合分析用户行为数据,挖掘潜在的市场需求和用户偏好,同时确保各自平台的用户隐私数据不被泄露。具体实现时,可将安全多方计算的加密、解密和计算逻辑封装成 Spark 的自定义算子(Operator),融入 Spark 的 RDD(Resilient Distributed Datasets)或 DataFrame 处理流程中,实现高效、安全的分布式计算。如图 2 所示,展示了安全多方计算与 Spark 融合的架构图:三、安全多方计算的高级应用场景3.1 医疗数据共享与联合研究医疗领域积累了海量的患者数据,这些数据蕴含着巨大的医学价值,但由于患者隐私保护和严格的法规限制,数据共享与联合研究面临重重困难。安全多方计算技术为这一困境提供了破局之道。多家医疗机构可以借助安全多方计算,在不泄露患者个人隐私的前提下,联合开展疾病研究、药物研发等工作。例如,针对罕见病的研究,不同地区的医院可以联合分析患者的基因数据、临床症状、治疗记录等信息,通过安全多方计算挖掘疾病的潜在致病基因和有效的治疗方案。据权威研究表明,采用安全多方计算进行医疗数据联合研究后,疾病研究的效率提高了 30%,新药研发周期平均缩短了 20%,为攻克疑难病症带来了新的希望。在一项针对罕见病的跨国联合研究项目中,来自 5 个国家的 10 家顶级医院参与其中。通过安全多方计算技术,这些医院在不泄露患者隐私的情况下,共享了超过 5000 份患者病例数据。经过联合分析,研究团队成功发现了一种与该罕见病相关的新基因靶点,基于此开发的新型治疗药物已进入临床试验阶段,有望为全球数千名患者带来治愈的可能。从社会人文角度来看,安全多方计算促进医疗数据共享,让更多患者受益于先进医疗研究成果,减少医疗资源分配不均带来的影响,体现了技术对人文关怀的促进作用。例如,偏远地区的患者可以通过安全多方计算参与到国际前沿的医疗研究中,获得更精准的诊断和治疗方案。某偏远地区的医院与国际知名医疗机构合作,利用安全多方计算技术共享患者数据,成功为一位罕见病患者制定了个性化治疗方案,使患者病情得到有效控制,生活质量显著提高。这种技术的应用不仅改善了患者的健康状况,还增强了患者对医疗系统的信任,体现了科技发展对社会公平和人文关怀的积极影响。3.2 金融风控联合建模在金融领域,风险评估和风控建模是保障金融稳定的核心任务,而这需要大量多维度的数据支持。不同金融机构如银行、保险公司、消费金融公司等持有各自客户的信用数据、消费行为数据、资产数据等,但出于数据安全和商业竞争的考虑,难以直接共享数据。安全多方计算技术使得金融机构能够在不泄露客户敏感信息的情况下,联合进行风险评估模型的训练和优化。通过安全多方计算,各机构可以共同分析客户数据,建立更精准的风险评估模型,有效降低金融风险。在实际应用中,某大型金融集团采用安全多方计算进行金融风控联合建模后,风险评估的准确率提高了 15%,不良贷款率降低了 10%,显著提升了金融风险管理水平。该金融集团旗下拥有银行、证券、保险等多个子公司,以往各子公司独立进行风险评估,数据孤立且模型不够精准。引入安全多方计算技术后,各子公司在保护客户隐私的前提下,共享部分数据进行联合建模。例如,银行提供客户的信贷记录,保险公司提供客户的理赔数据,证券子公司提供客户的投资行为数据。通过整合这些多维度数据,新的风险评估模型能够更全面地评估客户风险,为金融决策提供更可靠的依据。随着金融科技的发展,安全多方计算与区块链、人工智能等前沿技术的融合趋势逐渐显现。区块链可用于确保参与方身份验证和数据不可篡改,人工智能则能助力更精准的风险预测,这种跨领域融合为金融行业带来创新变革,提升金融服务的普惠性和安全性。例如,在普惠金融领域,通过安全多方计算和区块链技术,金融机构可以更准确地评估小微企业的信用风险,为其提供更合理的贷款额度和利率,促进小微企业的发展。某地区多家银行和小额贷款公司合作开展普惠金融项目,它们利用安全多方计算技术,整合各自掌握的小微企业的交易流水、纳税记录、社保缴纳等数据,同时借助区块链确保数据来源可靠且不可篡改。经过联合分析,原本被传统金融机构拒之门外的许多小微企业获得了合理的贷款,贷款额度平均提升了 20%,利率降低了 15%,有力地推动了当地小微企业的发展,促进了就业和经济增长。此外,人工智能算法能够对安全多方计算产生的海量金融数据进行深度挖掘,发现潜在的风险模式和市场趋势。例如,利用机器学习算法对客户的消费行为、还款记录等数据进行分析,提前预测客户的违约风险,为金融机构及时采取风险防范措施提供支持。四、安全多方计算面临的挑战与解决方案4.1 性能效率问题安全多方计算由于涉及复杂的密码学计算和频繁的网络通信,计算和通信开销较大,导致性能效率成为制约其广泛应用的瓶颈。为提升性能,可从以下几个方面着手:优化密码学算法:采用更高效的同态加密算法变体,如基于格的同态加密算法,在保证安全性的前提下,大幅减少加密和解密的计算量。基于格的同态加密算法利用格的数学性质,能够在较短的计算时间内完成加密和解密操作,相比传统同态加密算法效率更高。例如,在处理大规模数据时,传统同态加密算法可能需要数小时才能完成加密,而基于格的同态加密算法可以将时间缩短至几十分钟,大大提高了计算效率。研究表明,在处理 10GB 的数据集时,基于格的同态加密算法的加密时间仅为传统算法的 1/5,且在安全性上能够抵御量子计算攻击。分布式计算架构优化:设计更合理的分布式集群架构,利用多节点的并行计算能力,将复杂计算任务分解为多个子任务并行执行,提高整体计算效率。例如,采用主从架构结合分布式缓存技术,减少数据传输次数,提高计算速度。在一个包含 100 个节点的分布式集群中,通过优化架构和使用分布式缓存,数据传输时间减少了 50%,计算任务的完成时间缩短了 30%。通过合理分配计算任务和优化数据存储方式,使各节点能够充分发挥其计算能力,避免出现节点负载不均衡的情况。网络通信优化:采用高速网络通信协议和优化的数据传输策略,减少数据传输延迟和带宽消耗,如使用 UDP 协议进行部分数据传输,并结合数据压缩技术降低传输数据量。在数据传输前,对数据进行压缩处理,可有效减少传输时间和带宽占用。实验表明,对 1GB 的数据进行压缩后再传输,传输时间可缩短 70%,带宽占用降低 80%。选择合适的压缩算法,如 Snappy、Gzip 等,根据数据特点和网络环境进行优化配置,以实现最佳的传输效果。4.2 安全漏洞与攻击防范安全多方计算系统面临着诸多安全威胁,如中间人攻击、恶意参与者攻击、数据泄露攻击等。为有效防范这些攻击,需采取以下措施:强化密码学协议安全性:持续研究和改进密码学协议,修复潜在的安全漏洞,增强协议的抗攻击能力,如对混淆电路协议进行优化,防止电路结构被破解。通过引入随机化机制和多重加密技术,提高混淆电路协议的安全性。例如,在混淆电路协议中加入随机噪声,使得攻击者难以通过分析电路结构获取原始数据。研究人员不断提出新的密码学协议和改进方案,如基于不经意传输扩展的混淆电路协议,进一步提高了协议的安全性和效率。多方认证机制:引入严格的多方认证机制,确保参与计算的各方身份真实可靠,防止恶意节点混入。可采用基于数字证书的认证方式,结合区块链技术实现身份信息的不可篡改和可追溯。区块链的分布式账本特性,能将各方身份信息以加密形式存储在多个节点,确保数据的真实性和完整性。一旦身份信息被篡改,区块链的共识机制会立即检测到异常,保障计算环境的安全可靠。例如,在一个多方参与的医疗数据共享项目中,通过基于区块链的数字证书认证,成功阻止了一次恶意节点试图冒充医疗机构参与计算的攻击。利用区块链的智能合约技术,实现自动化的身份验证和权限管理,提高认证的效率和安全性。审计跟踪技术:建立完善的审计跟踪系统,记录计算过程中的关键操作和数据流向,便于及时发现和追溯潜在的安全问题。系统可以记录每次数据加密、传输、计算以及解密的时间、参与方、操作类型等关键信息。通过对审计日志的实时分析,能够快速检测到异常行为。例如,当监测到某个参与方在短时间内发起大量不合理的计算请求,或者数据传输量远超正常范围时,系统可自动触发警报,安全团队能迅速介入,对该参与方进行进一步审查,查明异常原因,采取相应的防范措施,如暂时中断该参与方的计算任务,对其身份和操作进行详细核实。采用大数据分析技术对审计日志进行深度挖掘,发现潜在的安全威胁和异常模式,提前预警并防范安全风险。五、技术发展的多维洞察5.1 前沿技术趋势下的安全多方计算在科技飞速发展的当下,量子计算技术的崛起给传统密码学带来了前所未有的挑战,安全多方计算自然也无法置身事外。基于量子 - resistant 密码学的安全多方计算协议,正成为学术界和产业界共同关注的焦点。以格密码为例,它基于复杂的格理论构建,独特的数学结构赋予其卓越的抗量子攻击特性。在未来量子计算普及的时代,格密码有望成为安全多方计算的中流砥柱,确保数据在计算和传输过程中的安全性。目前,许多科研团队正在研究基于格密码的安全多方计算协议的优化与应用拓展,力求在保障安全性的同时,提升计算效率和实用性。例如,某知名科研机构的研究团队成功将基于格密码的安全多方计算协议应用于金融数据的跨境传输与计算,在保证数据安全的同时,实现了比传统协议快 2 倍的计算速度。该研究成果为金融机构在全球化业务中保护数据安全提供了新的解决方案,降低了计算成本和时间成本。与此同时,联邦学习与安全多方计算的融合也在不断深化。联邦学习允许多个参与方在不直接共享原始数据的情况下协同训练模型,而安全多方计算则为这种协作提供了坚实的数据隐私保护屏障。在医疗领域,多家医院可以利用联邦学习与安全多方计算的结合,共同训练疾病诊断模型,在保护患者隐私的同时,提升模型的准确性和泛化能力。通过整合不同医院的病例数据进行联合建模,模型能够学习到更广泛的疾病特征,从而提高诊断的准确性,为患者提供更可靠的医疗服务。据某医学研究报告显示,采用联邦学习与安全多方计算结合的方式训练的疾病诊断模型,准确率比单一医院训练的模型提高了 12%。在实际应用中,通过联邦学习与安全多方计算的融合,能够整合不同地区、不同医院的医疗数据,解决数据孤岛问题,提高医疗资源的利用效率。5.2 跨领域融合驱动的创新变革安全多方计算与区块链的融合,为数据处理带来了全新的信任机制。区块链的去中心化特性确保了计算过程不受单一节点控制,不可篡改的账本则保证了数据和计算结果的真实性与可追溯性。在政务数据共享中,各部门可以通过这种融合技术,安全地共享和协同处理数据,提高政务服务的效率和透明度。不同政府部门之间共享公民的社保、税务、医疗等数据时,利用安全多方计算保护公民隐私,同时借助区块链确保数据的准确性和完整性,避免数据被恶意篡改,提升政府部门间的协作效率,为公民提供更便捷的一站式政务服务。例如,某城市通过采用安全多方计算与区块链融合技术,实现了社保、医保、民政等部门的数据共享,办理社保相关业务的时间从原来的平均 7 个工作日缩短至 3 个工作日。该城市的市民在办理社保转移、医保报销等业务时,不再需要在多个部门之间来回奔波提交材料,只需在一个平台上即可完成所有业务办理,大大提高了政务服务的便捷性和满意度。安全多方计算与人工智能的融合同样展现出巨大潜力。人工智能算法能够对安全多方计算产生的海量数据进行深度挖掘,提取其中的潜在价值。例如,在智能安防领域,通过安全多方计算共享监控数据,利用人工智能进行图像识别和行为分析,既能保障数据隐私,又能提高安防系统的智能化水平。将不同区域的监控数据在安全多方计算的框架下进行整合分析,人工智能算法可以实时监测异常行为,如人员聚集、异常闯入等,及时发出警报,有效提升公共安全保障能力。在某大型活动安保项目中,采用安全多方计算与人工智能融合技术,成功预警并处理了多起潜在的安全事件,保障了活动的顺利进行。通过人工智能算法对监控数据进行实时分析,能够快速准确地识别出异常行为,为安保人员提供及时的决策支持,提高安保工作的效率和效果。5.3 技术演进对社会人文的深远影响安全多方计算技术的发展,深刻地改变着社会的隐私观念和数据治理模式。在个人层面,它为人们参与线上服务提供了更可靠的隐私保护。当个人在进行在线医疗咨询、金融交易等活动时,涉及敏感信息的数据通过安全多方计算进行处理,大大降低了隐私泄露的风险,增强了人们对数字社会的信任。用户在进行线上医疗问诊时,个人的病历、症状等敏感信息在加密状态下传输和处理,医生能够获取必要的诊断信息,却无法获取患者的其他隐私细节,保障了患者的隐私安全,让患者更放心地使用在线医疗服务。据某市场调研机构的调查显示,在采用安全多方计算技术处理数据的在线医疗平台上,用户满意度提升了 25%。这表明安全多方计算技术不仅保护了用户的隐私,还提升了用户体验,促进了在线医疗服务的发展。从全球视角来看,安全多方计算促进了数据的合法合规流通,打破了数据孤岛。不同国家和地区的科研机构可以借助这一技术共享科研数据,加速科研进展,推动人类社会在医学、环保、能源等多个领域的共同进步。在医学研究中,跨国合作研究罕见病时,各国科研机构可以通过安全多方计算共享患者数据和研究成果,共同探索疾病的治疗方法,缩短研发周期,为全球患者带来福音。在商业领域,它也为企业间的合作创新提供了可能,促进了全球商业生态的繁荣与发展 ,体现了技术对社会公平和创新活力的积极推动作用。例如,某国际科研合作项目通过安全多方计算技术,整合了来自 5 个国家的科研数据,成功研发出一种新型药物,为全球患者带来了新的治疗选择。该项目的成功不仅展示了安全多方计算技术在科研合作中的重要作用,还促进了不同国家科研机构之间的交流与合作,推动了全球医学研究的发展。结束语:亲爱的 Java 和 大数据爱好者们,通过对 Java 大数据安全多方计算的深度剖析,我们全面掌握了其核心技术、Java 实现方法、丰富的应用场景、面临的挑战及解决方案,以及技术发展的多维洞察。安全多方计算作为大数据时代数据安全的坚固盾牌,有力地推动了跨领域数据合作与创新发展。然而,随着技术的不断演进和应用场景的日益复杂,我们仍需持续探索和创新,不断优化技术性能,强化安全防护体系。————————————————原文链接:https://blog.csdn.net/atgfg/article/details/145233339
  • [专题汇总] 2025年开年的第一篇合集来了。速进
    大家好,2025年开年的第一篇合集,本次带来的是Python,Java,MySql,Golang,JSON,等等希望可以帮到大家。1.Python判断for循环最后一次的方法【转】https://bbs.huaweicloud.com/forum/thread-0248173698858425071-1-1.html2.使用Python实现高效的端口扫描器【转】https://bbs.huaweicloud.com/forum/thread-0248173699028101072-1-1.html3.使用Python实现操作mongodb详解【转】https://bbs.huaweicloud.com/forum/thread-02109173699263711070-1-1.html4.一文详解Python中数据清洗与处理的常用方法【转】https://bbs.huaweicloud.com/forum/thread-02109173699342905071-1-1.html5.Go中sync.Once源码的深度讲解【转】https://bbs.huaweicloud.com/forum/thread-0271173699402065058-1-1.html6.从源码解析golang Timer定时器体系【转】https://bbs.huaweicloud.com/forum/thread-0251173701525255062-1-1.html7.golang1.23版本之前 Timer Reset方法无法正确使用【转】https://bbs.huaweicloud.com/forum/thread-02127173701584637057-1-1.html8.Python文件读写实用方法小结【转】https://bbs.huaweicloud.com/forum/thread-02104173701685566070-1-1.html9.mysql外键创建不成功/失效如何处理【转】https://bbs.huaweicloud.com/forum/thread-02109173701958630072-1-1.html10.Redis的Zset类型及相关命令详细讲解【转】https://bbs.huaweicloud.com/forum/thread-02109173702031434073-1-1.html11.大数据小内存排序问题如何巧妙解决【转】https://bbs.huaweicloud.com/forum/thread-02127173702077058058-1-1.html12.Redis多种内存淘汰策略及配置技巧分享【转】https://bbs.huaweicloud.com/forum/thread-0272173702166312062-1-1.html13.MySQL通过binlog实现恢复数据【转】https://bbs.huaweicloud.com/forum/thread-02109173702268081074-1-1.html14.MySQL如何将一个表的字段更新到另一个表中【转】https://bbs.huaweicloud.com/forum/thread-0272173702328248063-1-1.html15.JSON字符串转成java的Map对象详细步骤【转】https://bbs.huaweicloud.com/forum/thread-02109173702572327075-1-1.html
  • [技术干货] 大数据小内存排序问题如何巧妙解决【转】
    大数据小内存排序问题,很经典,很常见,类似的还有比如 “如何对上百万考试的成绩进行排序” 等等。三种方法:数据库排序(对数据库设备要求较高)分治法(常见思路)位图法(Bitmap)方法概要数据库排序(对数据库设备要求较高)操作:将数据全部导入数据库,建立索引,数据库对数据进行排序,提取出数据。特点:操作简单, 运算速度较慢,对数据库设备要求较高。分治法(常见思路)操作:操作与归并排序的思想类似,都是分治。将数据进行分块,然后对每个数据块进行内部的排序(假如是对int形数据升序)。和归并排序类似,每个数据块取第一个数据(当前块的最小数据),然后比较取出的数据,取其最小加入结果集。重复2操作,直到取完所有数据,此时排序完毕。特点:位图法(Bitmap)操作:基本思想就是利用一位(bit)代表一个数字,例如第 3 位上为 1,则说明 3 这个数字出现过,若为0,则说明 3 这个数字没有出现过。很简单~​ java.util 封装了 BitSet 这样一个类,是位图法的典型实现。特点:可读性差(不是一般的差 🤔)位图存储的元素个数虽然比一般做法多,但是存储的元素大小受限于存储空间的大小。要想定义存储空间大小就需要实现知道存储的元素到底有多少对于有符号类型的数据,需要用 2 位来表示,比如 第 0 位和第 1 位表示 0 这个数据,第 2 位和第 3 位表示 1 这个数据......,这会让位图能存储的元素个数,元素值大小上限减半只知道元素是否出现,无法知道出现的具体次数
  • [技术干货] Volatile关键字在Java并发编程中的应用与解析
    个示例中,我们使用一个volatile修饰的布尔变量作为线程间的标志位,来控制一个线程的执行和停止。 public class VolatileExample { // 使用volatile修饰的布尔变量,作为线程间的标志位 private volatile boolean running = true; public static void main(String[] args) { VolatileExample example = new VolatileExample(); // 启动一个工作线程 Thread workerThread = new Thread(example::doWork); workerThread.start(); // 主线程休眠一段时间后,修改标志位以停止工作线程 try { Thread.sleep(5000); // 休眠5秒 } catch (InterruptedException e) { e.printStackTrace(); } example.stopWork(); // 确保工作线程已停止 try { workerThread.join(); // 等待工作线程结束 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Worker thread has stopped."); } // 工作线程执行的方法 private void doWork() { while (running) { // 模拟工作负载 System.out.println("Worker is running..."); try { Thread.sleep(1000); // 每秒打印一次 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 保留中断状态 break; // 退出循环 } } System.out.println("Worker has stopped working."); } // 修改标志位以停止工作线程 public void stopWork() { running = false; // 修改volatile变量,确保对所有线程可见 } } 在这个示例中,running变量被声明为volatile,以确保当主线程修改其值时,工作线程能够立即感知到这个变化。工作线程在doWork方法中不断检查running变量的值,并根据其值决定是否继续执行模拟的工作负载。主线程在启动工作线程后,休眠5秒钟,然后调用stopWork方法来修改running变量的值,从而通知工作线程停止工作。六、结论volatile关键字在Java并发编程中扮演着重要角色,它确保了变量的可见性和禁止了指令的重排序。然而,它也存在一些局限性,如无法保证复合操作的原子性和可能增加内存开销等。因此,在使用volatile关键字时,需要谨慎考虑其适用场景和限制条件。在更复杂的并发场景中,可能需要使用synchronized关键字或其他并发工具类来确保线程安全。通过深入理解volatile关键字的特性和使用场景,并结合实际的代码示例进行实践,我们可以更好地利用Java并发编程的强大功能,构建出高效、稳定、可扩展的并发应用程序。
  • [技术干货] Linux:线程控制
    一、线程库        在Linux中,内核中并没有很明确的线程概念,而是只有轻量级进程的概念!!因此OS并没有给我们提供线程的系统调用,只会给我们提供轻量级进程的系统调用——>可是我们的用户只认识线程而不认识什么轻量级进程啊!!而且使用起来的学习成本也很高啊! 因此就有大佬在应用层为轻量级进程接口进行封装,为用户提供直接的线程接口(pthread线程库)         pthread线程库又叫原生线程库,几乎所有的Linux平台都是默认自带这个库的,但是他对于g++来说属于第三方库,链接这些线程函数库时要使用编译器命令的“-lpthread”选项!二、线程创建pthread_create功能:创建一个新的线程int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);参数thread:返回线程ID(输出型参数)attr:设置线程的属性,attr为NULL表示使用默认属性(一般设为NULL)start_routine:是个函数地址,线程启动后要执行的函数(其实就是通过要执行的函数来给线程划分地址空间)arg:传给线程启动函数的参数(可以通过类传多个)返回值:成功返回0;失败返回错误码(pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通 过返回值返回)       pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码(局部存储)。对于pthreads函数的错误, 建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小 2.1 简单看看多线程 为什么-l就可以了呢??——>因为这个库已经默认安装在系统路径下了,编译器知道他在哪,只是不知道要链接哪个库而已!!   如果我们想查看所有的轻量级进程的话  可以用 ps -aL(a是all的意思,L是轻的意思)        我们会发现主线程的PID和LWP(cpu调度的基本单位)是一样的,这应该也是用来让CPU区分切换的是主线程还是次线程的一个标识!!监视线程的方法: 2.2 全局变量  所有的线程都可以看到全局变量       我们会发现以前的进程间通信,无论是管道、共享内存、消息队列……他们让两个进程看到同一份代码和资源的方法都比较麻烦,可以线程天生就具有看到同一份资源的能力,所以也给我们的通信提供了很好的应用场景和技术准备!! 问题:为什么我们不研究多进程并发,而是研究多线程并发呢??——>因为多进程的写时拷贝、通信……都很麻烦,而线程的共享性更容易实现,他是先进性的表现,但是方便的同时也伴随着线程之间的相互影响、健壮性差等问题,因此这些需要我们程序员在代码上去解决这类问题!!2.3 tid vs LWP  我们会发现tid和LWP差距非常大,因为lwp是操作系统的轻量级进程的概念,只需要OS知道就行,而tid是给用户使用的!(本质是一个地址)!  2.4 线程函数参数返回值为啥都是void*以往进程返回是通过返回错误码来告知我们错误信息,可以线程中的函数为什么会是void*呢??因为不止可以传整形、字符串……还可以传类对象!! (类里面可以放很多内置类型,其实就相当于可以传很多参数,以及返回很多返回值)即使你只想传一个整形或者字符串,你也可以封装在类里面传,能传类的话尽量传类,因为他具有可扩展性!未来想增加别的类型就很方便! 比方说我们要计算1-100相加,我们可以写个request的类传递给他1-100的区间,然后再写个Respond的类帮助我们把运行结果返回回来!! 要注意一定不要在主线程里面创建局部变量传递给次线程!!        如果我们主线程要传类对象给次线程,就必须在堆区开辟空间,这样虽然td指针被释放了,但是我们可以通过args把这个指针传递给线程,这样每个线程就可以去访问自己在堆中的对象了!     其实堆区的资源大家都看得到,比如我2号线程也可以去看1号线程堆区的数据,但是这样没有意义!!所以线程可以看到全部的堆空间,但是每个线程访问的是堆的不同位置!!问题:可是我们为什么不直接在线程里去写这个参数,而是要让主线程通过类传递过去呢??——> 因为主线程可能需要给不止一个次线程分配任务,比如说我想让1线程算1-100,让2线程算101-200…… 也就是可以让每个线程并行地去共同完成同一个任务,而我只需要讲需要处理的数据通过类告诉他们就行,最后我再对结果进行汇总(主线程重分配和管理,次线程重实践)——>甚至你还可以把方法都写进类里面!!这样你的线程就更简洁了!!——>你次线程需要什么类,需要什么方法,我可以通过类来告诉你!!你只管调用就行! 三、线程等待pthread_ join你主线程把我新线程创建出来了,你不得管我吗??万一我还没退你先退了怎么办??——>所以我们要尽量保证主线程最后退!怎么让主线程最后退呢??你可能会想到写个死循环,然后把工作都交给次线程去干,这是这样真的好吗??我只是想让你管理我,不是想让你当甩手掌柜然后自己啥代码也不执行,而且我要是自己退了,你就搁那傻傻循环啥也不管吗??你难道不关心我的运行结果吗??你难道不需要释放我的空间吗??——>所以你主线程必须要等待子线程!!(1、将已经退出的线程的空间释放掉 2、创建新的线程时不会复用刚在退出线程的地址空间)功能:等待线程结束int pthread_join(pthread_t thread, void **value_ptr);参数:thread:线程IDvalue_ptr:它指向一个指针,后者指向线程的返回值(得知新线程的运行情况)返回值:成功返回0;失败返回错误码       调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的 终止状态是不同的 总结如下:1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。 2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。 3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传pthread_exit的参数。4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。 问题:为什么是void**呢?? ——> 因为OS作为管理者也需要知道执行结果,这个执行结果会先被携带结构体里,然后我们可以通过二级指针将我们自己的void*变量地址传递给他,然后把他拷贝过来!!四、线程分离pthread_detach和pthread_self           默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join(需要由主线程回收)操作,否则无法释放 资源,从而造成系统泄漏。       如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。 讲个小故事理解分离:比如说五六十年代那个时候,很多家庭里面会一次性有很多小孩由父母管理,然后这些小孩长大以后,有其中一个小孩跟父亲特别不对付(一种情况是小孩自己提出分家——子线程自己想分离  还有一种情况是父亲嫌弃你让你离开——父线程要求你分离),而这个小孩虽然可以共享家里的一部分资源,但是其实已经不是一家人了!!所以不管你以后怎么样了,父亲都不会管你了(就相当于线程分离之后,虽然他还可以用到进程的公共资源,并且他也有自己idea资源,但是父线程已经不关心他了  此时以后不管怎么都没人管他  只能自生自灭由OS去回收他)所以可以由线程组内的其他线程对目标线程进行分离,也可以是线程自己分离!! pthread_detach:int pthread_detach(pthread_t thread);  pthread_self pthread_t pthread_self(void); 可以获得线程自身的ID joinable和分离是冲突的,一个线程不能既是joinable又是分离的。——本质上就是将我们线程库中我们认为的tcp结构体里的一个关于线程是否分离的标记位给改了!!五、线程终止pthread_exit和pthread_cancel只终止某个线程而不终止整个进程,可以有三种方法:1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。2. 线程可以调用pthread_ exit终止自己。3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程pthread_exit函数功能:线程终止 void pthread_exit(void *value_ptr);参数value_ptr:value_ptr不要指向一个局部变量(独立栈空间会被释放)。返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(线程都终止了返回没有意义) pthread_cancel函数功能:取消一个执行中的线程 int pthread_cancel(pthread_t thread);参数thread:线程ID返回值:成功返回0;失败返回错误码六、c++的线程库 C++其实也有自己的线程库thread ! 可实际上他的底层也是封装的pthread的原生线程库!也需要指定链接!       而以往我们在windows系统下在vs中使用线程库,我们其实并不需要这样,这是因为windows下他有自己专门的线程库,因为windows实现的时候就是有专门的tcb结构体,所以我们包cpp头文件的时候,他的底层其实是windows类型的系统调用!!——>cpp具有跨平台性,根据不同的平台(Linux和windows),他用的是条件编译,外面虽然呈现出来的头文件和接口是一样的,但是不同的平台内部封装所使用的系统调用是不一样的!!——>所以在Linux下的cpp底层封装的是Linux的原生线程库(由于是用的进程模拟线程,所以并没有专门的tcb结构体,他的系统调用接口只有轻量级进程的概念,所以又封装了一个原生线程库给我们,而使用第三方库都需要链接,cpp底层也是这个原生线程库,所以也要链接)  而windows下的线程库就是原生windows下的系统调用,所以他并没有第三方库的概念!! ——>所以你平时写代码在不同的环境下没有感觉,是写库文件的设计者帮助你把这种差异给屏蔽掉了!! 所以我们平时刚推荐使用语言里的库方法而非系统调用接口,因为这样代码就不具备可移植性和跨平台性了!! 七、用户级线程vs内核级LWP用户级线程和内核级LWP是1:1的关系  线程共享进程数据,但也拥有自己的一部分数据:线程ID一组寄存器(保存上下文)独立栈 (完成调用链)errno (局部存储)信号屏蔽字调度优先级      进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户id和组id1、 线程的概念是库给我们维护的!!所以线程库注定要维护多个线程属性集合!——>先描述再组织2、不用维护线程的执行流,这是由OS的轻量级进程完成的(已经帮我们封装了) 3、原生线程库必然要被加载到内存中,因此我们的线程属性集合也应该在线程库中维护4、线程控制块就是库帮我们维护的一个用户级线程结构体tcb,而pthread_t类型的线程ID,本质 就是一个进程地址空间上的一个地址,就是指向他的。  7.1 独立线程栈       执行流的本质就是独立的调用链!! 所以每个线程都需要建立自己独立的调用链,所以就必须得有一个独立的栈结构——>支持我们在应用层来完成我们整个调用链对应的临时空间的开辟和释放——>这样对于局部变量来说,就可以保持线程的独立性        虽然是独立栈,但其实其他线程想要访问在技术角度也是可以做到的(定义一个全局的指针,然后在某一个线程中让他保存其中的一个局部变量的地址,然后主线程再当全部线程创建完成之后,再去查看这个全局的指针变量),因为线程与线程之间几乎没有秘密!! 7.2 局部存储 如果我们想要一个只属于线程的全局变量呢??——>通过局部存储(他会被存储在一个区域中  )!   问题:可是这看上去很鸡肋啊!!干嘛要定义这种私有的全局变量啊,我直接在自己的独立栈定义局部变量不就行了??——>可是如果你的线程内部将来也调用函数了呢??比如说你想让别的函数也能够知道你线程的id或者是其他属性,那你还得把这个局部变量通过参数传递给他!! 所以局部存储私有的全局变量最核心的意义就是可以让该线程独立栈内部调用链上所有的函数都可以看得到这些信息,而不需要传参或者是频繁地调用系统调用!————————————————                        原文链接:https://blog.csdn.net/weixin_51142926/article/details/142829794
  • [技术干货] Java 报错:找不到或无法加载主类
    引言在 Java 开发过程中,经常会遇到“找不到或无法加载主类”(Error: Could not find or load main class)的错误。这个错误通常表示 JVM 无法找到指定的主类,可能是由于类路径(Classpath)设置不正确、类文件缺失、编译错误等原因引起的。1. 错误描述当运行 Java 应用程序时,如果 JVM 无法找到指定的主类,会抛出以下错误:Error: Could not find or load main class <ClassName>其中 <ClassName> 是你尝试运行的主类名称。2. 常见原因以下是导致“找不到或无法加载主类”错误的一些常见原因:2.1 类路径设置错误类路径未包含主类:确保类路径(Classpath)中包含了主类所在的目录或 JAR 文件。类路径格式错误:确保类路径的格式正确,特别是多个路径之间的分隔符(Windows 使用 ;,Linux 使用 :)。2.2 类文件缺失编译错误:确保所有 Java 源文件已经成功编译,并且生成了相应的 .class 文件。文件路径错误:确保主类文件位于正确的目录中,且文件名和类名一致。2.3 主类声明错误缺少 public static void main(String[] args) 方法:确保主类中有一个 public static void main(String[] args) 方法,这是 JVM 入口点。类名拼写错误:确保命令行中指定的类名与实际类名完全一致,包括大小写。2.4 JAR 文件问题JAR 文件损坏:确保 JAR 文件没有损坏,并且包含所需的类文件。MANIFEST 文件错误:如果使用 JAR 文件,确保 MANIFEST 文件中的 Main-Class 属性正确指定了主类。3. 诊断方法以下是诊断“找不到或无法加载主类”错误的一些方法:3.1 检查类路径打印类路径:在命令行中使用 echo %CLASSPATH%(Windows)或 echo $CLASSPATH(Linux)命令,检查当前的类路径设置。手动验证:确保类路径中包含了主类所在的目录或 JAR 文件。3.2 检查类文件编译源文件:重新编译所有 Java 源文件,确保生成了 .class 文件。检查文件路径:确保主类文件位于正确的目录中,且文件名和类名一致。3.3 检查主类声明查看源代码:打开主类的源代码文件,确保有 public static void main(String[] args) 方法。检查类名:确保命令行中指定的类名与实际类名完全一致,包括大小写。3.4 检查 JAR 文件验证 JAR 文件:使用 jar tf <jar-file> 命令检查 JAR 文件中的内容,确保包含所需的类文件。检查 MANIFEST 文件:打开 JAR 文件中的 MANIFEST.MF 文件,确保 Main-Class 属性正确指定了主类。4. 解决方案根据诊断结果,采取相应的解决方案:4.1 修正类路径设置类路径:在命令行中使用 -cp 或 -classpath 参数指定类路径。例如:java -cp .;path/to/classes com.example.MainClass环境变量:确保 CLASSPATH 环境变量正确设置。例如,在 Windows 中:set CLASSPATH=.;path\to\classes4.2 重新编译类文件编译源文件:使用 javac 命令重新编译所有 Java 源文件。例如:javac -d . com/example/MainClass.java4.3 修正主类声明添加 main 方法:确保主类中有一个 public static void main(String[] args) 方法。例如:package com.example; public class MainClass {    public static void main(String[] args) {        System.out.println("Hello, World!");    }}检查类名:确保命令行中指定的类名与实际类名完全一致,包括大小写。例如:java com.example.MainClass4.4 修复 JAR 文件重新打包 JAR 文件:使用 jar 命令重新打包 JAR 文件。例如:jar cvf myapp.jar -C path/to/classes .更新 MANIFEST 文件:确保 MANIFEST.MF 文件中的 Main-Class 属性正确指定了主类。例如:Main-Class: com.example.MainClass5. 示例以下是一个完整的示例,展示了如何编译和运行一个简单的 Java 应用程序:5.1 创建源文件创建一个名为 MainClass.java 的文件,内容如下:package com.example; public class MainClass {    public static void main(String[] args) {        System.out.println("Hello, World!");    }}5.2 编译源文件在命令行中导航到源文件所在目录,编译源文件:mkdir -p com/examplemv MainClass.java com/example/javac -d . com/example/MainClass.java5.3 运行应用程序确保类路径设置正确,运行应用程序:java -cp . com.example.MainClass6. 总结“找不到或无法加载主类”错误通常是由于类路径设置错误、类文件缺失、主类声明错误或 JAR 文件问题引起的。通过仔细检查类路径、类文件、主类声明和 JAR 文件,可以快速定位和解决这个问题。————————————————                        原文链接:https://blog.csdn.net/2401_85648342/article/details/143706066
  • [技术干货] Java 中的 LocalDateTime、DateTime 和 Date 的区别解析
    在 Java 中,处理日期和时间是开发中常见的任务之一,特别是在涉及到多个时区、日期格式、时间计算等需求时。Java 提供了多种方式来处理日期和时间,其中 LocalDateTime、DateTime 和 Date 是三种常见的日期时间类。尽管它们看起来有些相似,但它们的设计理念和应用场景却各有不同。本文将深入分析这三者的区别,帮助大家更好地理解它们的使用场景。一、LocalDateTime:新的 Java 8 日期时间 API1.1 LocalDateTime 简介LocalDateTime 是 Java 8 中引入的 java.time 包的一部分,它代表了没有时区信息的日期和时间。它只包含 年、月、日、时、分、秒、纳秒 信息,不涉及与时区或具体的时间点相关的数据。1.2 设计理念LocalDateTime 设计的目标是解决传统 java.util.Date 类中存在的许多问题,提供一个清晰、直观的 API 来处理日期和时间。由于它没有时区信息,它非常适合表示 本地时间,例如在某个特定地点的时间,且不受时区转换的影响。1.3 适用场景本地日期时间处理:例如,我们只关心某个事件发生的日期和时间,但不关心该事件发生的时区。与数据库交互:当你存储和操作不涉及时区的日期时间(比如某些日历系统或事务记录)时,LocalDateTime 是一个理想的选择。1.4 示例代码import java.time.LocalDateTime;import java.time.format.DateTimeFormatter; public class LocalDateTimeExample {    public static void main(String[] args) {        LocalDateTime now = LocalDateTime.now();  // 获取当前的本地日期和时间        System.out.println("当前本地日期时间: " + now);         // 格式化输出        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");        System.out.println("格式化后的日期时间: " + now.format(formatter));    }}输出结果:当前本地日期时间: 2024-11-13T12:45:30.123456789格式化后的日期时间: 2024-11-13 12:45:30二、DateTime:没有明确标准的类2.1 DateTime 的模糊性在 Java 标准库中,并没有直接命名为 DateTime 的类。通常,在一些库或框架中,DateTime 用来指代 日期和时间的组合。例如,Java 8 的 ZonedDateTime 和 LocalDateTime 都可能被通称为 DateTime,尽管它们是不同的类。因此,DateTime 并没有明确的标准定义,通常只是作为一个通用术语,用来描述所有涉及日期和时间的类。2.2 适用场景在一些第三方库(如 Joda-Time)或框架中,DateTime 被广泛用于表示日期和时间。如果使用这些库,可能会遇到 DateTime 这个类。在 Java 中,如果遇到 DateTime,我们可能需要进一步查看它具体是哪个类,例如 ZonedDateTime 或 OffsetDateTime。三、Date:老旧的日期时间类3.1 Date 简介java.util.Date 是 Java 中最早的日期和时间类之一。它代表自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数,直到 Java 8 被新的日期时间 API 取代。Date 类最初设计上存在诸多缺陷,因此不再推荐用于新的项目开发。3.2 设计缺陷时区问题:Date 类本身并不包含时区信息,且会根据所在的系统时区进行转换,导致时区处理不够精确。不便的 API:许多方法(如 getYear())的返回值并不直观,而且对日期的操作不便,缺少流畅的日期计算方法。精度不足:Date 的精度仅限于毫秒,不能处理纳秒级别的时间。3.3 适用场景兼容旧代码:如果你需要与旧版的 API 进行交互,Date 可能仍然是不可避免的。时间戳:由于 Date 内部使用毫秒表示时间,因此它仍然适用于一些需要表示时间戳的场景。3.4 示例代码import java.util.Date; public class DateExample {    public static void main(String[] args) {        Date date = new Date();  // 获取当前日期和时间        System.out.println("当前日期时间: " + date);    }}输出结果:当前日期时间: Wed Nov 13 12:45:30 GMT 2024四、如何选择合适的日期时间类?特性    LocalDateTime    DateTime(通常指 ZonedDateTime、OffsetDateTime 等)    Date(旧的 java.util.Date)时区信息    无时区信息    ZonedDateTime 有时区信息,LocalDateTime 没有时区信息    Date 依赖于系统时区精确度    纳秒精度    视具体类型而定,如 ZonedDateTime 支持到纳秒精度    毫秒精度设计方式    Java 8 引入的新日期时间 API,推荐使用    DateTime 不是标准类,通常是指 LocalDateTime 或 ZonedDateTime 等    旧的 API,设计不够清晰,容易出错功能    只包含日期和时间,没有时区    包含日期和时间,可以有时区(如 ZonedDateTime)    仅表示一个时间点,设计较为原始使用推荐    推荐用于不需要时区的日期时间操作    ZonedDateTime 或 OffsetDateTime 用于处理带时区的日期时间    不推荐使用,除非需要兼容旧代码1. LocalDateTime:适用于本地日期和时间的场景,不需要时区处理。2. ZonedDateTime:适用于需要时区处理的场景,适合跨时区的日期时间计算。3. Date:仅在兼容旧代码或需要处理时间戳时使用,不推荐在新项目中使用。在 Java 8 之后,我更推荐使用新的日期时间 API(java.time 包中的类),这些类设计更加清晰,功能更强大,避免了 java.util.Date 中的很多问题。————————————————                        原文链接:https://blog.csdn.net/Y_1215/article/details/143755420
  • [技术干货] Java stream流中peek用法详解
    在Java中,Stream是一种用于处理集合数据的强大工具。它提供了一种函数式编程的方式来对数据进行操作和转换。Stream中的peek方法是一种非终端操作,它允许你在流的每个元素上执行一个操作,而不会改变流的内容。peek方法的语法如下:Stream<T> peek(Consumer<? super T> action)其中,action是一个接收一个元素并执行操作的函数。peek方法的主要作用是在流的每个元素上执行一个操作,比如打印元素的值、记录日志、调试等。它通常用于调试和观察流的中间状态,而不会对流的内容进行修改。下面是一个使用peek方法的简单示例:List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> doubledNumbers = numbers.stream() .peek(n -> System.out.println("Processing number: " + n)) .map(n -> n * 2) .collect(Collectors.toList());在上面的示例中,我们创建了一个整数列表numbers,然后通过流的方式对每个元素进行处理。在流的peek操作中,我们打印了每个数字的值。然后,我们使用map操作将每个数字乘以2,并将结果收集到一个新的列表中。当我们运行上面的代码时,会看到以下输出:Processing number: 1 Processing number: 2 Processing number: 3 Processing number: 4 Processing number: 5通过使用peek方法,我们可以观察到流中每个元素的处理过程。这对于调试和理解流的中间状态非常有用。需要注意的是,peek方法是一个中间操作,它不会触发流的终端操作。如果你希望对流的内容进行修改或者获取最终的结果,你需要在peek方法之后添加一个终端操作,比如collect、forEach等。总结起来,peek方法是一个在流的每个元素上执行操作的非终端操作。它通常用于调试和观察流的中间状态,而不会对流的内容进行修改。原文链接:https://gitcode.csdn.net/65e957c41a836825ed78f822.html
  • [技术干货] Java——Stream流的peek方法详解
    Java 8 中引入了Stream API,极大地简化了集合操作,使得开发者可以使用流的方式进行数据处理。Stream 提供了一系列非常强大的操作方法,其中之一就是 peek() 方法。peek() 是一个中间操作,它可以用来在操作流的过程中查看元素的处理状态。本文将详细介绍 peek() 方法的使用场景和原理,并配合代码示例帮助大家深入理解。一、peek() 方法简介peek() 方法的定义在 java.util.stream.Stream 接口中,其签名如下:Stream<T> peek(Consumer<? super T> action);作用:peek() 是一个中间操作,它允许我们在流的每个元素上执行一个操作,但并不会改变流中的元素或中断流的处理。常用作调试工具,用来在流的各个操作步骤中查看流中的数据。它接收一个 Consumer 函数作为参数,Consumer 函数可以对每个流中的元素执行某些动作。特点:peek() 不会消耗流,只是执行一个旁路行为。因为是中间操作,它不会触发终端操作,因此在调用完 peek() 后,还需要调用诸如 forEach()、collect() 这类终端操作来触发流的处理。示例代码:import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;public class PeekExample {    public static void main(String[] args) {        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);        // 使用peek方法调试流操作过程        List<Integer> result = numbers.stream()                .filter(n -> n % 2 == 0)  // 过滤出偶数                .peek(n -> System.out.println("Filtered: " + n))  // 查看过滤结果                .map(n -> n * n)  // 对偶数进行平方                .peek(n -> System.out.println("Mapped: " + n))  // 查看映射结果                .collect(Collectors.toList());  // 收集结果        System.out.println("最终结果: " + result);    }}输出结果:Filtered: 2Mapped: 4Filtered: 4Mapped: 16最终结果: [4, 16]在上面的示例中,peek() 用来查看流中元素的处理情况,展示了在经过 filter() 和 map() 操作后的数据变化。二、peek() 方法的常见使用场景2.1 调试流操作peek() 的主要用途之一是调试。当我们处理复杂的流操作链时,可能很难理解每个中间操作的效果。这时,可以通过 peek() 来查看流中的数据在每个操作后的变化,以便找到问题或验证逻辑是否正确。import java.util.Arrays;import java.util.List;public class DebugWithPeek {    public static void main(String[] args) {        List<String> words = Arrays.asList("apple", "banana", "cherry", "date");        words.stream()                .filter(w -> w.length() > 4)                .peek(w -> System.out.println("Filtered: " + w))                .map(String::toUpperCase)                .peek(w -> System.out.println("Mapped to upper case: " + w))                .forEach(System.out::println);    }}输出结果:Filtered: appleMapped to upper case: APPLEFiltered: bananaMapped to upper case: BANANAFiltered: cherryMapped to upper case: CHERRYAPPLEBANANACHERRY可以看到,peek() 方法被用于调试,以便我们看到 filter() 和 map() 操作后的字符串。2.2 记录日志在实际应用中,peek() 还可以用于记录流操作的执行过程,比如将流中每个元素的处理结果写入日志。这在数据处理链条较长时,尤为有用。import java.util.Arrays;import java.util.List;import java.util.logging.Logger;public class LogWithPeek {    private static final Logger logger = Logger.getLogger(LogWithPeek.class.getName());    public static void main(String[] args) {        List<Integer> numbers = Arrays.asList(10, 20, 30, 40, 50);        numbers.stream()                .filter(n -> n > 20)                .peek(n -> logger.info("After filter: " + n))                .map(n -> n / 2)                .peek(n -> logger.info("After map: " + n))                .forEach(System.out::println);    }}在这个例子中,peek() 被用于记录日志,通过 Logger 的 info() 方法记录流中每个元素的处理状态。2.3 数据检查与验证peek() 还可以用来对流中的数据进行检查与验证。当你想确认流中数据是否符合某种规则,但不希望中断流的处理时,peek() 是一个非常好的选择。import java.util.Arrays;import java.util.List;public class DataValidationWithPeek {    public static void main(String[] args) {        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");        names.stream()                .filter(name -> name.length() > 3)                .peek(name -> {                    if (name.startsWith("C")) {                        System.out.println("注意!名字以C开头: " + name);                    }                })                .forEach(System.out::println);    }}在这个示例中,peek() 方法用于检查名字是否以字母C开头,而不影响流的其他操作。三、与forEach()的区别peek() 和 forEach() 看似相似,都是用来对流中的元素进行操作,但它们有明显的区别:peek() 是中间操作,而 forEach() 是终端操作。peek() 通常用于调试或数据检查,因为它不会中断流的链式操作;而 forEach() 是用来最终消费流的元素。示例代码:import java.util.Arrays;import java.util.List;public class PeekVsForEach {    public static void main(String[] args) {        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);        // 使用peek()作为中间操作        numbers.stream()                .peek(n -> System.out.println("Peeked: " + n))                .map(n -> n * 2)                .forEach(System.out::println);        System.out.println("--------");        // 使用forEach()作为终端操作        numbers.stream()                .map(n -> n * 2)                .forEach(n -> System.out.println("ForEach: " + n));    }}输出结果:Peeked: 1Peeked: 2Peeked: 3Peeked: 4Peeked: 5--------ForEach: 2ForEach: 4ForEach: 6ForEach: 8ForEach: 10可以看到,peek() 用于在流操作中查看每个元素,而 forEach() 用于最终消费元素。四、注意事项惰性求值:peek() 是中间操作,具有惰性,只有在终端操作(如 forEach()、collect())调用时,流的处理才会被执行。不可用于修改流元素:peek() 不能修改流中的元素,它只用于执行副作用操作。如果需要修改元素的值,应使用 map() 方法。适用场景:peek() 最适合用于调试或监控流的中间状态,不应该滥用,否则可能会导致代码可读性降低。五、总结在Java的Stream API中,peek() 方法是一个强大的工具,它允许我们在流的处理中观察和调试数据,特别是在数据处理链比较长的情况下,它可以帮助我们跟踪流中元素的状态和变化。但需要注意的是,peek() 不能用于修改流的元素,更多地是用作调试、记录日志和数据检查的手段。通过丰富的代码示例,我们了解了peek() 的常见使用场景和注意事项。在实际开发中,合理使用peek() 可以极大地帮助我们调试和监控流操作,希望本文能帮助你深入理解并掌握peek()的使用。————————————————                        原文链接:https://blog.csdn.net/qq_42978535/article/details/142763452
总条数:764 到第
上滑加载中