)
案例SpringBoot当在浏览器地址栏访问前端静态页面http://localhost:8080/usre.html后在前端页面上会发送ajax请求请求服务端http://localhost:8080/list服务端程序加载 user.txt 文件中的数据读取出来后最终给前端页面响应json格式的数据前端页面再将数据渲染展示在表格中。代码实现1.准备工作1.导入HTML界面文件和user.txt用户信息文件2.定义封装用户信息的实体类。在com.xizao下再定义一个包pojo专门用来存放实体类。 在该包下定义一个实体类User3.开发服务端程序接收请求读取文本数据并响应在案例中需要读取文本中的数据并且还需要将对象转为json格式所以这里呢我们在项目中再引入一个非常常用的工具包hutool。 然后调用里面的工具类就可以非常方便快捷的完成业务操作。在com.xizao包下新建一个子包controller在其中创建一个UserController各代码段解析InputStream in this.getClass().getClassLoader().getResourceAsStream(User.txt);从类路径中加载 User.txt 文ArrayListString lines IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList());使用 Hutool 工具类的 IoUtil.readLines() 方法将文件的每一行读取到 ArrayList 中使用 UTF-8 字符集编码防止中文乱码Listuser userList lines.stream().map(line - {String[] part line.split(,);Integer id Integer.parseInt(part[0]);String username part[1];String password part[2];String name part[3];Integer age Integer.parseInt(part[4]);LocalDateTime updateTime LocalDateTime.parse(part[5], DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss));return new user(id,username,password,name,age,updateTime);}).toList();使用 Stream 流 处理每一行数据map() 方法将每行文本转换为 user 对象假设文件格式为id,用户名密码姓名年龄更新时间用逗号分隔最后 toList() 将所有 user 对象收集为 Listreturn userList;将 user 列表返回由于有 RestController 注解Spring Boot 会自动将 List 转换为 JSON 格式关键字详解1. InputStream含义输入流用于从数据源如文件读取字节数据作用建立程序与文件之间的数据通道2. this.getClass().getClassLoader()this当前对象实例getClass()获取对象的 Class 对象getClassLoader()获取类加载器作用从项目的 classpath通常是 resources 目录加载资源文件3. getResourceAsStream()含义获取资源的输入流作用以流的形式读取资源文件内容4. StandardCharsets.UTF_8含义标准字符集常量表示 UTF-8 编码作用确保读写文件时使用统一的编码避免乱码5. stream() 重要含义将集合转换为流作用支持对集合进行函数式操作如过滤、映射等示例lines.stream() 将 ArrayList 转为 Stream6. map() 重要含义映射转换作用将流中的每个元素转换为另一种形式示例将 String文本行→ user对象7. - Lambda 表达式含义Lambda 操作符作用简化匿名内部类的写法示例line - { ... } 等价于重写一个接受 line 参数的方法8. split(,)含义字符串分割方法作用按逗号将一行文本拆分为字符串数组示例1,zhangsan,123,张三20,2024-01-01 12:00:00.split(,) → [1, zhangsan, 123, 张三, 20, 2024-01-01 12:00:00]9. Integer.parseInt()含义将字符串转换为 int 类型作用类型转换示例Integer.parseInt(20) → 20int 类型10. LocalDateTime.parse()含义解析日期时间字符串作用将文本格式的日期转换为 LocalDateTime 对象示例LocalDateTime.parse(2024-01-01 12:00:00, 格式化器)11. DateTimeFormatter.ofPattern()含义创建日期格式化器作用指定日期的解析/格式化模式示例yyyy-MM-dd HH:mm:ss 表示 2024-01-01 12:00:00 格式12. new user(...)含义构造方法调用作用创建 user 对象实例13. toList() Java 16含义收集流元素到不可变列表作用将 Stream 转为 List注意如果是 Java 8-15需用 .collect(Collectors.toList())14. RestController含义RESTful 风格控制器注解作用标识该类为控制器且方法返回值自动转为 JSON15. RequestMapping(/list)含义请求映射注解作用当访问 /list 路径时执行 list() 方法3.启动服务测试访问http://localhost:8080/user.html访问成功获取数据访问http://localhost:8080/user.html代表访问resource.static下的user.html静态文件2.分层解耦1.三层架构1.问题引入上述案例的功能我们虽然已经实现但是呢我们会发现案例中解析文本文件中的数据处理数据的逻辑代码给页面响应的代码全部都堆积在一起了全部都写在controller方法中了。当前程序的这个业务逻辑比较简单的如果业务逻辑再稍微复杂一点Controller方法的代码量会变得很大。当我们要修改操作数据部分的代码需要改动Controller当我们要完善逻辑处理部分的代码需要改动Controller当我们需要修改数据响应的代码还是需要改动Controller这样呢就会造成我们整个工程代码的复用性比较差而且代码难以维护。 解决这个问题在现在的开发中有非常成熟的解决思路那就是分层开发。2.三层架构在我们进行程序设计以及程序开发时尽可能让每一个接口、类、方法的职责更单一些单一职责原则。单一职责原则一个类或一个方法就只做一件事情只管一块功能。这样就可以让类、接口、方法的复杂度更低可读性更强扩展性更好也更利于后期的维护。分析下之前的程序上述案例的处理逻辑呢从组成上看可以分为三个部分数据访问负责业务数据的维护操作包括增、删、改、查等操作。逻辑处理负责业务逻辑处理的代码。请求处理、响应数据负责接收页面的请求给页面响应数据。按照上述的三个组成部分在我们项目开发中呢可以将代码分为三层如图所示按照上述的三个组成部分在我们项目开发中呢可以将代码分为三层如图所示Controller控制层。接收前端发送的请求对请求进行处理并响应数据。Service业务逻辑层。处理具体的业务逻辑。Dao数据访问层(Data Access Object)也称为持久层。负责数据访问操作包括数据的增、删、改、查。基于三层架构的程序执行流程如图所示前端发起的请求由Controller层接收Controller响应数据给前端Controller层调用Service层来进行逻辑处理Service层处理完后把处理结果返回给Controller层Serivce层调用Dao层逻辑处理过程中需要用到的一些数据要从Dao层获取Dao层操作文件中的数据Dao拿到的数据会返回给Service层思考按照三层架构的思想如果要对业务逻辑(Service层)进行变更会影响到Controller层和Dao层吗答案不会影响。 程序的扩展性、维护性变得更好了3.代码分解我们使用三层架构思想来改造下之前的程序控制层包名com.xizao.controller业务逻辑层包名com.xizao.service数据访问层包名com.xizao.dao3.1控制层接收前端发送的请求对请求进行处理并响应数据在com.xizao.controller中创建UserController类代码如下3.2 业务逻辑层处理具体的业务逻辑在com.xizao.service中创建UserSerivce接口代码如下为在com.xizao.service.impl中创建UserSerivceImpl接口代码如下3.3. 数据访问层负责数据的访问操作包含数据的增、删、改、查在com.xizao.dao中创建UserDao接口代码如下在com.xizao.dao.impl中创建UserDaoImpl接口代码如下3.4具体实现流程三层架构的好处复用性强便于维护利用扩展2.分层解耦2.1问题分析由于我们现在在程序中需要什么对象直接new一个对象new UserServiceImpl()。如果说我们需要更换实现类比如由于业务的变更UserServiceImpl 不能满足现有的业务需求我们需要切换为 UserServiceImpl2 这套实现就需要修改Contorller的代码需要创建 UserServiceImpl2 的实现new UserServiceImpl2()。Service中调用Dao也是类似的问题。这种呢我们就称之为层与层之间耦合了。 那什么是耦合呢 首先需要了解软件开发涉及到的两个概念内聚和耦合。内聚软件中各个功能模块内部的功能联系。耦合衡量软件中各个层/模块之间的依赖、关联的程度。软件设计原则高内聚低耦合。高内聚指的是一个模块中各个元素之间的联系的紧密程度如果各个元素(语句、程序段)之间的联系程度越高则内聚性越高即 高内聚。低耦合指的是软件中各个层、模块之间的依赖关联程序越低越好。目前层与层之间是存在耦合的Controller耦合了Service、Service耦合了Dao。而 高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。那最终我们的目标呢就是做到层与层之间尽可能的降低耦合甚至解除耦合。2.2解决办法不能new就意味着没有业务层对象程序运行就报错解决思路提供一个容器容器中存储一些对象(例UserService对象)Controller程序从容器中获取UserService类型的对象将要用到的对象交给一个容器管理。应用程序中用到这个对象就直接从容器中获取现在的问题来是我们如何将对象交给容器管理呢 程序运行时容器如何为程序提供依赖的对象呢我们想要实现上述解耦操作就涉及到Spring中的两个核心概念控制反转Inversion Of Control简称IOC。对象的创建控制权由程序自身转移到外部容器这种思想称为控制反转。对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为IOC容器或Spring容器。依赖注入Dependency Injection简称DI。容器为应用程序提供运行时所依赖的资源称之为依赖注入。程序运行时需要某个资源此时容器就为其提供这个资源。例EmpController程序运行时需要EmpService对象Spring容器就为其提供并注入EmpService对象。bean对象IOC容器中创建、管理的对象称之为bean对象。3.IOCDI入门3.1. 将Service及Dao层的实现类交给IOC容器管理在实现类加上Component注解就代表把当前类产生的对象交给IOC容器管理。3.2. 为Controller 及 Service注入运行时所依赖的对象4.IOC详解4.1bean的声明前面我们提到IOC控制反转就是将对象的控制权交给Spring的IOC容器由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。在之前的入门案例中要把某个对象交给IOC容器管理需要在类上添加一个注解Component而Spring框架为了更好的标识web应用程序开发当中bean对象到底归属于哪一层又提供了Component的衍生注解注解说明位置Component声明bean的基础注解不属于以下三类时用此注解ControllerComponent的衍生注解标注在控制层类上ServiceComponent的衍生注解标注在业务层类上RepositoryComponent的衍生注解标注在数据访问层类上由于与mybatis整合用的少那么此时我们就可以使用Service注解声明Service层的bean。 使用Repository注解声明Dao层的bean。 代码实现如下4.2组件扫描问题使用前面学习的四个注解声明的bean一定会生效吗答案不一定。原因bean想要生效还需要被组件扫描前面声明bean的四大注解要想生效还需要被组件扫描注解ComponentScan扫描。该注解虽然没有显式配置但是实际上已经包含在了启动类声明注解SpringBootApplication中默认扫描的范围是启动类所在包及其子包。所以我们在项目开发中只需要按照如上项目结构将项目中的所有的业务类都放在启动类所在包的子包中就无需考虑组件扫描问题。5.DI详解依赖注入是指IOC容器要为应用程序去提供运行时所依赖的资源而资源指的就是对象。在入门程序案例中我们使用了Autowired这个注解完成了依赖注入的操作而这个Autowired翻译过来叫自动装配。Autowired注解默认是按照类型进行自动装配的去IOC容器中找某个类型的对象然后完成注入操作入门程序举例在Controller运行的时候就要到IOC容器当中去查找Service这个类型的对象而我们的IOC容器中刚好有一个Service这个类型的对象所以就找到了这个类型的对象完成注入操作。5.1Autowired用法Autowired 进行依赖注入常见的方式有如下三种1). 属性注入RestControllerpublic class UserController {//方式一: 属性注入Autowired private UserService userService;}优点代码简洁、方便快速开发。缺点隐藏了类之间的依赖关系、可能会破坏类的封装性。2). 构造函数注入RestControllerpublic class UserController {//方式二: 构造器注入private final UserService userService;Autowired //如果当前类中只存在一个构造函数, Autowired可以省略public UserController(UserService userService) {this.userService userService;}}优点能清晰地看到类的依赖关系、提高了代码的安全性。缺点代码繁琐、如果构造参数过多可能会导致构造函数臃肿。注意如果只有一个构造函数Autowired注解可以省略。通常来说也只有一个构造函数3). setter注入/*** 用户信息Controller*/RestControllerpublic class UserController {//方式三: setter注入private UserService userService;Autowired public void setUserService(UserService userService) {this.userService userService;}}优点保持了类的封装性依赖关系更清晰。缺点需要额外编写setter方法增加了代码量。在项目开发中基于Autowired进行依赖注入时基本都是第一种和第二种方式。官方推荐第二种方式因为会更加规范但是在企业项目开发中很多的项目中也会选择第一种方式因为更加简洁、高效在规范性方面进行了妥协。5.2注意事项那如果在IOC容器中存在多个相同类型的bean对象会出现什么情况呢如果存在两个UserService的实现类并且都交给了IOC容器管理此时启动项目会发现控制台会报错了出现错误的原因呢是因为在Spring的容器中UserService这个类型的bean存在两个框架不知道具体要注入哪个bean使用所以就报错了。如何解决上述问题呢Spring提供了以下几种解决方案PrimaryQualifierResource方案一使用Primary注解当存在多个相同类型的Bean注入时加上Primary注解来确定默认的实现。PrimaryServicepublic class UserServiceImpl implements UserService { }方案二使用Qualifier注解指定当前要注入的bean对象。 在Qualifier的value属性中指定注入的bean的名称。 Qualifier注解不能单独使用必须配合Autowired使用。RestControllerpublic class UserController {Qualifier(userServiceImpl)Autowired private UserService userService;}方案三使用Resource注解是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。RestControllerpublic class UserController {Resource(name userServiceImpl)private UserService userService;}面试题Autowird 与 Resource的区别Autowired 是spring框架提供的注解而Resource是JDK提供的注解Autowired 默认是按照类型注入而Resource是按照名称注入