SpringBootWeb案例——分层解耦

发布于 2 天前 0 次阅读 2778 字 预计阅读时间: 13 分钟 JAVA


Lombok作用

// 不使用Lombok - 需要大量样板代码
public class User {
    private Long id;
    private String name;
    private Integer age;
    
    // 需要手动编写所有getter/setter
    // 需要手动编写toString()
    // 需要手动编写equals()和hashCode()
    // 需要手动编写构造函数
}

// 使用Lombok - 代码极其简洁
@Data                       // 自动生成getter/setter/toString/equals/hashCode
@AllArgsConstructor        // 全参数构造函数
@NoArgsConstructor         // 无参数构造函数
@Builder                   // 建造者模式
public class User {
    private Long id;
    private String name;
    private Integer age;
    @NonNull              // 非空检查
    private String email;
}

Hutool的作用

//IoUtil.readLines():一行代码读取文件所有行到List中
public List<String> findAll() {
    // 使用Hutool的IoUtil.readLines方法
    InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
    ArrayList<String> lines = IoUtil.readLines(in, "utf-8", new ArrayList<>());
    return lines;
}
// 如果不使用Hutool,需要手动编写:
public List<String> findAll() {
    InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
    List<String> lines = new ArrayList<>();
    
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"))) {
        String line;
        while ((line = reader.readLine()) != null) {
            lines.add(line);
        }
    } catch (IOException e) {
        throw new RuntimeException("读取文件失败", e);
    }
    
    return lines;
}

@ResponseBody

  • 类型:方法注解、类注解
  • 位置:书写在Controller方法上或类上
  • 作用:将方法返回值直接响应给浏览器,如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器

为什么Controller中,只在类上添加了@RestController注解、方法添加了@RequestMapping注解,并没有使用@ResponseBody注解,却能给浏览器响应?

这是因为,类上加了@RestController注解,而这个注解是由两个注解组合起来的,分别是:@Controller 、@ResponseBody。 那也就意味着,类上已经添加了@ResponseBody注解了,而一旦在类上加了@ResponseBody注解,就相当于该类所有的方法中都已经添加了@ResponseBody注解。

分层解耦

高内聚

准确解释:一个模块内部的各个元素(代码、方法、类)功能相关性的程度。高内聚意味着:

  • 模块内的元素共同完成一个单一、明确的功能
  • 模块职责清晰,不做不相关的事情
  • 修改模块时,影响范围仅限于模块内部
// ❌ 低内聚的类 - 职责混杂
public class UserManager {
    // 用户管理相关
    public void addUser() {...}
    public void deleteUser() {...}
    
    // 邮件发送相关(不相关功能)
    public void sendEmail() {...}
    
    // 文件操作相关(不相关功能)
    public void uploadFile() {...}
}

// ✅ 高内聚的类 - 单一职责
public class UserService {
    // 只处理用户相关业务
    public void register() {...}
    public void login() {...}
    public void updateProfile() {...}
    public void resetPassword() {...}
}

public class EmailService {
    // 专门处理邮件相关
    public void sendEmail() {...}
}

public class FileService {
    // 专门处理文件相关
    public void uploadFile() {...}
}

低耦合

准确解释:模块之间依赖关系的强弱程度。低耦合意味着:

  • 模块之间依赖接口而不是具体实现
  • 修改一个模块时,对其他模块影响最小
  • 模块可以独立开发、测试和维护
//问题代码:高耦合
public class OrderService {
    // 问题1:直接创建具体对象(强绑定)
    private MySQLOrderDao orderDao = new MySQLOrderDao();  // 只能MySQL
    private EmailSender emailSender = new EmailSender();    // 只能发邮件
    
    public void createOrder() {
        orderDao.save(order);   // 问题2:如果换数据库要改代码
        emailSender.send();     // 问题3:如果改发短信要改代码
    }
}
  • OrderService 硬编码 依赖MySQL和Email
  • 想换成Redis数据库?❌ 必须改OrderService代码
  • 想发短信通知?❌ 必须改OrderService代码
  • 想单元测试?❌ 很难(因为依赖具体实现)
//解决方案:低耦合
// 📦 第一步:定义"标准协议"(接口)
// 就像外卖平台规定:所有饭店必须能"接单"和"送餐"
interface OrderDao {
    void save(Order order);  // 所有数据存储都必须实现这个方法
}

interface NotificationService {
    void notifyUser();  // 所有通知服务都必须实现这个方法
}

// 🏪 第二步:不同的"商家"(实现类)
// 比如:有专门用MySQL的店
class MySQLOrderDao implements OrderDao {
    @Override
    public void save(Order order) {
        System.out.println("使用MySQL保存订单");
    }
}

// 也有专门用Redis的店
class RedisOrderDao implements OrderDao {
    @Override
    public void save(Order order) {
        System.out.println("使用Redis保存订单");
    }
}

// 有发邮件的店
class EmailNotification implements NotificationService {
    @Override
    public void notifyUser() {
        System.out.println("发送邮件通知");
    }
}

// 有发短信的店
class SMSNotification implements NotificationService {
    @Override
    public void notifyUser() {
        System.out.println("发送短信通知");
    }
}

// 🛒 第三步:顾客(OrderService)只跟平台打交道
public class OrderService {
    // 只依赖"接口",不依赖具体实现
    private OrderDao orderDao;           // 可以接受任何实现了OrderDao的类
    private NotificationService notificationService;  // 可以接受任何通知服务
    
    // 构造器注入:使用什么实现,由外部决定
    public OrderService(OrderDao dao, NotificationService service) {
        this.orderDao = dao;
        this.notificationService = service;
    }
    
    public void createOrder() {
        // 只管调用标准接口,不管具体是谁实现的
        orderDao.save(order);
        notificationService.notifyUser();
    }
}

三层架构

  • Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
  • Service:业务逻辑层。处理具体的业务逻辑。
  • Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。

分层

其中规范是:
分为三个包,controller、dao、service;
包下存放接口和impl子包;
子包下存放对应实现类,类名为接口名+Impl。

解耦

首先不能在UserController中使用new对象

@RestController
public class UserController {

    UserService userService;//此处不new
    @RequestMapping("/list")
    public List<User> list(){
        List<User> userList = userService.findAll();
        return userList;
    }
}

将要用到的对象交给一个容器管理应用程序中用到这个对象,就直接从容器中获取

我们想要实现上述解耦操作,就涉及到Spring中的两个核心概念:

  • 控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
    • 对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器。
  • 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
    • 程序运行时需要某个资源,此时容器就为其提供这个资源。
    • 例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象。
  • bean对象:IOC容器中创建、管理的对象,称之为:bean对象。

IOC&DI入门

将Service及Dao层的实现类,交给IOC容器管理

在实现类加上 @Component 注解,就代表把当前类产生的对象交给IOC容器管理。

A. UserDaoImpl

@Component
public class UserDaoImpl implements UserDao {
    @Override
    public List<String> findAll() {
        InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
        ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
        return lines;
    }
}

B. UserServiceImpl

@Component
public class UserServiceImpl implements UserService {

    private UserDao userDao;

    @Override
    public List<User> findAll() {
        List<String> lines = userDao.findAll();
        List<User> userList = lines.stream().map(line -> {
            String[] parts = line.split(",");
            Integer id = Integer.parseInt(parts[0]);
            String username = parts[1];
            String password = parts[2];
            String name = parts[3];
            Integer age = Integer.parseInt(parts[4]);
            LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            return new User(id, username, password, name, age, updateTime);
        }).collect(Collectors.toList());
        return userList;
    }
}

为Controller 及 Service注入运行时所依赖的对象

A. UserServiceImpl

@Component
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;
    
    @Override
    public List<User> findAll() {
        List<String> lines = userDao.findAll();
        List<User> userList = lines.stream().map(line -> {
            String[] parts = line.split(",");
            Integer id = Integer.parseInt(parts[0]);
            String username = parts[1];
            String password = parts[2];
            String name = parts[3];
            Integer age = Integer.parseInt(parts[4]);
            LocalDateTime updateTime = LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            return new User(id, username, password, name, age, updateTime);
        }).collect(Collectors.toList());
        return userList;
    }
}

B. UserController

@RestController
public class UserController {
    
    @Autowired
    private UserService userService;

    @RequestMapping("/list")
    public List<User> list(){
        //1.调用Service
        List<User> userList = userService.findAll();
        //2.响应数据
        return userList;
    }

}

IOC详解

Bean的声明

注解说明位置
@Component声明bean的基础注解不属于以下三类时,用此注解
@Controller@Component的衍生注解标注在控制层类上
@Service@Component的衍生注解标注在业务层类上
@Repository@Component的衍生注解标注在数据访问层类上(由于与mybatis整合,用的少)

注意1声明bean的时候,可以通过注解的value属性指定bean的名字,如果没有指定,默认为类名首字母小写。

注意2使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。

组件扫描

  • 前面声明bean的四大注解,要想生效,还需要被组件扫描注解 @ComponentScan 扫描。
  • 该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解 @SpringBootApplication 中,默认扫描的范围是启动类所在包及其子包。

DI详解

1.@Autowired

@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)

@RestController
public class UserController {

    //方式一: 属性注入
    @Autowired
    private UserService userService;
    
  }
}
  • 优点:代码简洁、方便快速开发。
  • 缺点:隐藏了类之间的依赖关系、可能会破坏类的封装性。
@RestController
public class UserController {

    //方式二: 构造器注入
    private final UserService userService;
    
    @Autowired //如果当前类中只存在一个构造函数, @Autowired可以省略
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
 }   
  • 优点:能清晰地看到类的依赖关系、提高了代码的安全性。
  • 缺点:代码繁琐、如果构造参数过多,可能会导致构造函数臃肿。
  • 注意:如果只有一个构造函数,@Autowired注解可以省略。(通常来说,也只有一个构造函数)
/** * 用户信息Controller */
@RestController
public class UserController {
    
    //方式三: setter注入
    private UserService userService;
    
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    
}    
  • 优点:保持了类的封装性,依赖关系更清晰。
  • 缺点:需要额外编写setter方法,增加了代码量。

在项目开发中,基于@Autowired进行依赖注入时,基本都是第一种和第二种方式。(官方推荐第二种方式,因为会更加规范)但是在企业项目开发中,很多的项目中,也会选择第一种方式因为更加简洁、高效(在规范性方面进行了妥协)。

2.多个相同bean

在IOC容器中,存在多个相同类型的bean对象使用:

方案一:使用@Primary注解

当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。

@Primary
@Service
public class UserServiceImpl implements UserService {
}

方案二:使用@Qualifier注解

指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称,如@Service("userService")。 @Qualifier注解不能单独使用,必须配合@Autowired使用。

@RestController
public class UserController {

    @Qualifier("userServiceImpl")
    @Autowired
    private UserService userService;
 }

方案三:使用@Resource注解

按照bean的名称进行注入。通过name属性指定要注入的bean的名称。

@RestController
public class UserController {
        
    @Resource(name = "userServiceImpl")
    private UserService userService;
}

3.面试题:@Autowird 与 @Resource的区别

  • @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
  • @Autowired 默认是按照类型注入,而@Resource是按照名称注入