4 分层解耦
约 3996 字大约 13 分钟
2025-04-05
三层架构
- controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据。
- service:业务逻辑层,处理具体的业务逻辑。
- dao:数据访问层(DataAccessObject)(持久层),负责数据访问操作,包括数据的增、删、改、查。
高内聚低耦合
- 内聚:软件中各个功能模块内部的功能联系
- 耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
Bean的声明
要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解 | 说明 | 位置 |
---|---|---|
@Component | 声明bean的基础注解 | 不属于以下三类时,用此注解 |
@Controller | @Component的衍生注解 | 标注在控制器类上 |
@Service | @Component的衍生注解 | 标注在业务类上 |
@Repository | @Component的衍生注解 | 标注在数据访问类上 (由于与mybatis整合,用的少) |
原本是只使用Component注解,但后续为了区分清晰衍生了下面三个注解,只有当一个类不属于三层结构是使用Component |
- 声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
- 使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。
bean组件扫描
前面声明bean的四大注解,要想生效,还需要被组件扫描注解@componentScan扫描。 @ComponentScan注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中,默认扫描的范围是启动类所在包及其子包。
如果有的包放在不符合规范的地方无法被扫描到可以手动指定扫描路径,路径是相对于当前包上层的java包来说的
@ComponentScan({"dao","com.itheima")
Autowried
通过@Autowried注解开启自动注入
为了更好的分层解耦通常在开发中在上层类中声明下层类的接口,通过调用接口中的方法来进行工作(面向接口) 但单独的接口没有函数实体,需要有实体类来实现接口,这样在上层类中使用autowried自动装配可以自动将上层类中声明的接口装配实现类的函数,以后换实现了只需要更改生效的实体类即可 empService是一个接口
@RestController
public class EmpController{
@Autowired//运行时,IOC容器会提供该类型的bean对象,并赋值给该变量-依赖注入
private EmpService empService;
@RequestMapping("/listEmp")
public Result list(){}
}
但如果有多个实现类都实现了同一个接口且都被ioc容器所管控,那么autowried就无法识别装配哪个类从而报错,有以下三种解决方案
@Primary
@Primary
@Service
class EmpServiceImplA implments empService{}
@Service
class EmpServiceImplB implments empService{}
在需要使用的类上面加上@Primary注解这样注入的就是此类
@Qualifier
在@Autowired上面加上Qualifier注解指定被ioc容器所管理的实现类(指定类名首字母小写),即可让该接口注入指定类
@Qualifier("empServiceImplA")
@Autowired
private EmpService empService;
@Resource
Autowried注解默认是根据类型注入 可以使用Resource注解,它是jdk中的,默认是根据名称注入的
@Resource(name="empServiceImplB")
private EmpService empService;
@RequestMapping
用于路径路由
- 当写在一个方法上时表示在进入这条路径时调用此方法
- 当写在一个类上时,表示此类中的所有方法都是该路径下的,如果类中方法也有路径那么就是类上的requestmapping和方法上的requestmapping路径的拼接
合并路径 当一个类用来进行一个路径的操作时可以把前面相同的路径合并起来,这样每个方法中只需要在后面添加不同的路径部分进行路由
@Slf4j
@RestController
@RequestMapping("/depts")
public class DeptController {
@Autowired
private DeptService deptService;
@GetMapping
public Result list() {}
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {}
@PostMapping
public Result add(@RequestBody Dept dept) {}
}
和
@Slf4j
@RestController
public class DeptController {
@Autowired
private DeptService deptService;
/*查询部门数据*/
@GetMapping("/depts")
public Result list() {
log.info("查询所有部门数据");
List<Dept> deptList = deptService.list();
return Result.success(deptList);
}
/*删除部门数据*/
@DeleteMapping("/depts/{id}")
public Result delete(@PathVariable Integer id) {
log.info("根据id删除部门:{}", id);
deptService.delete(id);
return Result.success();
}
/*新增部门数据*/
@PostMapping("/depts")
public Result add(@RequestBody Dept dept) {
log.info("新增部门:{}", dept);
deptService.add(dept);
return Result.success();
}
}
是一样的
接收文件
springboot提供了存储文件的类 MultipartFile 接收到的文件被存放在中转区中,如果不取出当程序结束时会消失
MultipartFile中有一个成员函数transferTo由于将文件转存到目标文件中
在springboot中文件上传默认单个文件允许最大大小为1m,如果需要上传大文件需要进行配置
spring:
servlet:
multipart:
max-file-size: 10MB # 配置单个文件最大上传大小
max-request-size: 100MB # 配置单个请求最大上传大小(一次请求可以上传多个文件)
@PostMapping("/upload")
public Result uploadResult(String name, Integer age, MultipartFile image) throws IllegalStateException, IOException {
log.info("文件上传:{},{},", name, age, image);
String fileName = image.getOriginalFilename();
log.info("文件名:{},文件大小:{}", fileName, image.getSize());
//将文件转存到磁盘目录中
String externName=fileName.substring(fileName.lastIndexOf("."));
fileName=UUID.randomUUID().toString()+externName;
image.transferTo(new File("D:\\垃圾\\"+fileName));
return Result.success();
}
会话技术
http协议是无状态的,每次请求都是相互独立,不携带上次请求的数据
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
Cookie
cookie是存储在客户端的会话跟踪技术
在客户端向服务器发送请求后,服务器返回的响应字段的响应头中会携带一个Set-Cookie的字段,其中包含一个键值对,客户端接收到响应后会解析字段获取Cookie,此后每次发送请求都会携带这个cookie,服务器接收到请求后验证cookie是否有效
优点:
- HTTP协议中支持的技术
缺点:
- 移动端APP无法使用Cookie
- 不安全,用户可以自己禁用cookie
- Cookie不能跨域
跨域区分三个维度:协议、IP/域名、端口 如果有一个维度不一样都是跨域
Session
Session本质也是使用Cookie实现的
客户端向浏览器发送请求时如果是首次请求那么服务器就会创建一个新的Session对象,然后给客户端响应的数据中响应头的Set-Cookie字段被设置为session的id,这个cookie的键是固定的都是JSESSIONID,客户端接收到后进行解析,将解析的sessionid存储到本地,此后每次会话都会在Cookie字段携带sessionid,服务器接收到请求后解析如果有sessionid就在本地的sesssion对象池中查找此id如果有就进行正常通讯
优点:
- 存储在服务端,安全
缺点:
- 服务器集群环境下无法直接使用Session
- 也有Cookie的缺点
现在大多数企业都是使用服务器集群,即部署多个服务器,这样就不会出现因为一台服务器故障而导致的单点故障(只部署在一台服务器上,这台服务器故障了整个应用就无法访问),在集群前客户端发送的请求会先到达负载均衡服务器,负载均衡服务器根据各个集群服务器的负载来动态分配
令牌技术
客户端登录成功后,生成令牌.后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
优点:
- 支持PC端、移动端
- 解决集群环境下的认证问题
- 减轻服务器端存储压力 缺点:
- 需要自己实现
JWT令牌
定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
将原始的json数据进行了封装(jwt令牌是携带数据的)
例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.JTdCJTIybmFtZSUyMiUzQSUyMlRvbSUyMiUyQyUyMmlhdCUyMiUzQTE1MTYyMzkwMjIlNoQ=.SflKxwRJSMeKKF2QT4fwpMeJf...
解析
组成部分 | 示例 | 原始数据 |
---|---|---|
Header(头),记录令牌类型、签名算法等。 | eyJhbGciOiJIUzI1N iIsInR5cCI6IkpXVCJ9 | {"alg":"HS256","type":"JWT"} |
Payload(有效载荷),携带一些自定义信息、默认信息等。 | JTdCJTIybmFtZSUyMi UzQSUyMlRvbSUyMi UyQyUyMmlhdCUyMi UzQTE1MTYyMzkwMjIlNoQ= | {id":"1","username":"Tom"} |
Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。 | SflKxwRJSMeKKF2QT4fwpMeJf... | |
Base64:是一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式。 | ||
生成的jwt在签名部分使用了base64编码,因此解析出来的是经过base64编码的数据,还需要再次使用base64解码 | ||
前面两部分直接解析出来的就是原始数据 |
在java代码中生成jwt令牌需要引入外部依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
示例:
// 生成jwt
@Test
void testGenJwt() {
// 存储数据
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("name", "Tom");
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "PinkDopeyBug") // 设置签名算法
.setClaims(claims) // 自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) // 设置签名有效期为1小时
.compact();
System.out.println(jwt);
parseJwt(jwt);
}
// 解析jwt
@Test
void parseJwt(String jwt) {
Claims claims = Jwts.parser()
.setSigningKey("PinkDopeyBug")
.parseClaimsJws(jwt)
.getBody();
System.out.println(claims);
}
过滤器
Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。(不是属于spring的是javax包下的) springboot3不再支持javax了,可以使用jakarta
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
使用
要使用Filter需要定义一个类,实现Filter接口,并重写其所有方法 其中init函数相当于构造函数 destroy函数相当于析构函数 doFilter函数在每次拦截到请求后都会调用,用于过滤请求
Filter类上加@WebFilter注解,配置拦截资源的路径。 注解的参数是拦截什么样的资源
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("DemoFilter init执行");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){
System.out.println("拦截到了请求");
}
@Override
public void destroy(){
System.out.println("DemoFilter destroy执行");
}
}
ServletRequest和ServletResponse可以与 HttpServletRequest和HttpServletResponse之间进行强转
拦截路径 | urlPatterns值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问/login路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问问所有资源,都会被拦截 |
引导类(调用filter的类)上加@ServletComponentScan开启servlet组件支持
传入的FilterChain其中有个doFilter函数是用来放行的,当过滤后可以传入FilterRequest和FilterResponse放行请求
放行后访问对应资源,资源访问完成后还会再回到Filter中执行放行后的逻辑
过滤器链
一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
拦截器
概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
要在拦截器上方加上Component注解让spring管控 定义拦截器要实现HandlerInterceptor接口并重写所有方法
- preHandle : 目标资源方法执行前执行,返回true:放行,返回false:不放行
- poetHandle : 目标资源方法执行后执行
- afterCompletion : 视图渲染完毕后执行,最后执行 重写好方法后还需要注册拦截器,需要定义一个配置类,配置类需要实现WebMvcConfigurer接口 在配置类上需要加上Configuration表示当前是spring 的配置类 需要重写addInterceptors方法并在里面指明拦截条件 拦截器类
@Component
public class LoginCheckInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
System.out.println("拦截器拦截请求: preHandl---" + request.getRequestURI());
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView){
System.out.println("拦截器拦截请求: postHandle---" + request.getRequestURI());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){
System.out.println("拦截器拦截请求: afterCompletion---" + request.getRequestURI());
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");
}
}
addPathPatterns表示拦截哪个资源 excludePathPatterns表示排除哪些资源,可以传入多个参数
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配/depts/1 |
/** | 任意级路径 | 能匹配/depts,/depts/1,/depts/1/2 |
/depts/* | /depts下的一级路径 | 能匹配/depts/1,不能匹配/depts/1/2,/depts |
/depts/** | /depts下的任意级路径 | 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1 |
过滤器Filter会拦截所有的资源,而lnterceptor只会拦截Spring环境中的资源。
全局异常处理器
在一个项目各个层次会有许多异常,如果在各个层次都进行异常处理会使代码臃肿,可以使用抛出异常让上层接收直至最上层,然后最上层将异常抛给全局异常处理器同一处理
事务管理
spring提供了事务管理 使用@Transactional注解将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
- 作用在方法上表示这个方法由sping进行事务管理
- 作用在类上表示这个类中所有方法都交由spring进行事务管理
- 作用在接口上表示这个接口中的方法都交由spring进行事务管理
@Transactional中有两个属性rollbackFor和propagation
rollbackFor异常回滚
在@Transactional注解的函数中如果有抛出非运行时(runtime)异常就不会发生事务回滚
默认情况下,只有出现RuntimeException才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务。
@Transactional(rollbackFor = Exception.class)
表明出现所有异常都会进行事务回滚
propagation传播行为
事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
常见传播行为
属性值 | 含义 |
---|---|
REQUIRED | 【默认值】需要事务,如果调用它的方法有事务则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
在一个新的事务中只有在其中发生异常会回滚新事务不会影响老事务(创建新事务新老事务不是整体新事务回滚不会影响老事务) | |
如果是加入到事务那么只要有事务所在的事务就会回滚(加入后事务为一个整体) |
贡献者
版权所有
版权归属:PinkDopeyBug