Java笔记-15、Web后端基础 分层解耦
三层架构
满足单一职责原则。
- Controller控制层:接收前端发送的请求,对请求进行处理并响应数据。
- Service业务逻辑层:处理具体的业务逻辑。
- Dao数据访问层(持久层):负责数据访问操作,包括数据的增删改查。
- 浏览器发过来请求,首先访问Controller层,Controller调用Service层,Service层调用Dao层。架构例子如下。

三层架构的例子
// 一个读取文件数据并解析成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对象,而是使用一个容器管理实现类的对象,当需要用到的时候,由容器提供这个对象。
这个思路有两个问题:
- 如何将实现类的对象交给容器管理? 控制反转IOC
- 容器如何为应用提供需要依赖的对象? 依赖注入DI
IOC与DI
IOC DI入门
- 控制反转:Inversion of Control,简称IOC。对象的创建控制权由程序自身转移到外部(IOC容器或者叫Spring容器),这种思想称为控制反转。是Spring框架中的第一大核心。
- 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
- Bean对象:IOC容器中创建、管理的对象,称之为Bean。
依旧考虑上述代码例子,需要分层解耦,就需要完成:
- 将Dao和Service的实现类,交给IOC容器管理。
这一步需要给这两个实现类添加注解:@Component。 - 为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进行依赖注入的常见方式有三种。
- 属性注入
@RestController
public class UserController {
@Autowired
private UserService userService;
}
- 构造函数注入
@RestController
public class UserController {
private final UserService userService; // final提高安全性
@Autowired
public UserController(UserService userService){
this.userService = userService;
}
}
- 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,将会报错。
解决方案:
@Primary:在实现类上指定。
@Primary
@Service
public class UserServiceImpl2 implements UserService {
@Override
public List<User> List(){...}
}
@Qualifier+@Autowired:在注入时指定,配合@Autowired。
@RestController
public class UserController {
@Autowired
@Qualifier("userServiceImpl2")
private UserService userService;
}
@Resource:java自带注解,不是Spring提供的,在注入时指定。不需要@Autowired。
@RestController
public class UserController {
@Resource(name = "userServiceImpl2")
private UserService userService;
}

浙公网安备 33010602011771号