Java笔记-15、Web后端基础 分层解耦

三层架构

满足单一职责原则。

  1. Controller控制层:接收前端发送的请求,对请求进行处理并响应数据。
  2. Service业务逻辑层:处理具体的业务逻辑。
  3. Dao数据访问层(持久层):负责数据访问操作,包括数据的增删改查。
  • 浏览器发过来请求,首先访问Controller层,Controller调用Service层,Service层调用Dao层。架构例子如下。
    CleanShot 2025-02-13 at 23.38.24@2x

三层架构的例子

// 一个读取文件数据并解析成JSON的例子
@RestController
public class UserController {
    @RequestMapping("/list")
    public List<User> list() throws Exception {
        //1.加载并读取user.txt文件,获取用户
        InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
        ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
        //2.解析用户信息,封装User对象到List集合中
        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());

        //3.将List集合转换为JSON格式的字符串,并返回
        return userList;
    }
}

现在将其拆分。


public interface UserDao {
    /**
     * 加载用户数据
     * @return
     */
    public List<String> findAll();
}
///////////////////////////////////////
public class UserDaoImpl implements UserDao {
    @Override
    public List<String> findAll() {
        //1.加载并读取user.txt文件,获取用户
        InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");
        ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());
        return lines;
    }
}
///////////////////////////////////////
public interface UserService {
    /**
     * 查询所有用户信息
     * @return
     */
    public List<User> findAll();
}
///////////////////////////////////////
public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImpl(); // 出现了耦合
    @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;
    }
}
///////////////////////////////////////
@RestController
public class UserController {
    private UserService userService = new UserServiceImpl(); // 出现了耦合
    @RequestMapping("/list")
    public List<User> list() throws Exception {
        List<User> userList = userService.findAll();
        //3.将List集合转换为JSON格式的字符串,并返回
        return userList;
    }
}

分层解耦

耦合:衡量软件中各个层/各个模块的依赖关联程度。
内聚:软件中各个功能模块内部的功能联系。
软件设计原则:高内聚低耦合。

上述代码中有模块件的耦合,在调用Service层和Dao层时,new了对象。

private UserDao userDao = new UserDaoImpl();

解耦合的思路是:在调用Service层和Dao层时不需要new对象,而是使用一个容器管理实现类的对象,当需要用到的时候,由容器提供这个对象。

这个思路有两个问题:

  1. 如何将实现类的对象交给容器管理? 控制反转IOC
  2. 容器如何为应用提供需要依赖的对象? 依赖注入DI

IOC与DI

IOC DI入门

  • 控制反转:Inversion of Control,简称IOC。对象的创建控制权由程序自身转移到外部(IOC容器或者叫Spring容器),这种思想称为控制反转。是Spring框架中的第一大核心。
  • 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
  • Bean对象:IOC容器中创建、管理的对象,称之为Bean。

依旧考虑上述代码例子,需要分层解耦,就需要完成:

  1. 将Dao和Service的实现类,交给IOC容器管理。
    这一步需要给这两个实现类添加注解:@Component
  2. 为Controller 及 Service注入运行时所依赖的对象。
    这一步需要在成员变量上添加注解:@Autowired。应用程序运行时,会自动的查询该类型的bean对象,并赋值给该成员变量。
    @Autowired
    private UserDao userDao;

IOC详解

四大注解

要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:

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

在Springboot集成web开发中,声明控制器bean只能用@Controller。但一般Controller不用再加@Controller,因为@RestController已经封装了@Controller
容器中bean的默认名字就是类名首字母小写。

注解的生效

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

DI详解

注入方式

基于@Autowired进行依赖注入的常见方式有三种。

  1. 属性注入
@RestController
public class UserController {
    @Autowired
    private UserService userService;
}
  1. 构造函数注入
@RestController
public class UserController {
    private final UserService userService; // final提高安全性
    @Autowired
    public UserController(UserService userService){
        this.userService = userService;
    }
}
  1. setter注入
@RestController
public class UserController {
    private UserService userService;
    @Autowired
    public void setUserService(UserService userService){
        this.userService = userService;
    }
}

注入方式的选择

属性注入的优缺点

优点:代码简洁、方便快速开发。
缺点:隐藏了类之间的依赖关系、可能会破坏类的封装性。

按照Java面向对象来说,封装应该对成员变量有setter\getter方法进行赋值和获取,但加上@Autowired底层是通过反射对成员变量赋值,破坏了封装。

构造函数注入的优缺点

优点:能清晰地看到类的依赖关系、提高了代码的安全性。
缺点:代码繁琐、如果构造参数过多,可能会导致构造函数臃肿。

注意:如果只有一个构造函数,@Autowired注解可以省略。

setter注入的优缺点

优点:保持了类的封装性,依赖关系更清晰。
缺点:需要额外编写setter方法,增加了代码量。

官方推荐与实践选择
  • 在Spring的官方文档中,推荐使用构造函数注入。
  • 在开发实践中,大多数项目组使用的是属性注入。

注意事项

  • @Autowired注解,默认是按照类型进行注入的。
    如果存在多个相同类型的bean,将会报错。
    解决方案:
  1. @Primary:在实现类上指定。
@Primary
@Service
public class UserServiceImpl2 implements UserService {
    @Override
    public List<User> List(){...}
}
  1. @Qualifier+@Autowired:在注入时指定,配合@Autowired
@RestController
public class UserController {
    @Autowired
    @Qualifier("userServiceImpl2")
    private UserService userService;
}
  1. @Resource:java自带注解,不是Spring提供的,在注入时指定。不需要@Autowired
@RestController
public class UserController {
    @Resource(name = "userServiceImpl2")
    private UserService userService;
}
posted @ 2025-02-14 00:00  subeipo  阅读(46)  评论(0)    收藏  举报