JavaWeb基础2-2

JavaWeb基础2-2

六、Interceptor

1、概念

概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

1.定义拦截器,实现HandlerInterceptor接口, 并重写其所有方法。

2.注册拦截器

1
2
3
4
5
6
@Override // 目标资源方法执行前执行,放回true:放行,返回false: 不放行
public boolean preHandle
@Override // 目标资源方法执行后执行
public void postHandle
@Override //视图渲染完毕后执行,最后执行
public void afterCompletion

3.交给AOP容器管理,加上@component

2、入门

  1. 定义拦截器,实现HandlerInterceptor接口, 并重写其所有方法。
  2. 注册拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行, 返回true: 放行, 放回false, 不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//1.获取请求url。
String url = req.getRequestURL().toString();
log.info("请求的url: {}",url);

//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){
log.info("登录操作, 放行...");
return true;
}

//3.获取请求头中的令牌(token)。
String jwt = req.getHeader("token");

//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}

//5.解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}

//6.放行。
log.info("令牌合法, 放行");
return true;
}

@Override //目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle ...");
}

@Override //视图渲染完毕后运行, 最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}

3.拦截器-拦截路径

拦截器可以根据需求,配置不同的拦截路径:

4.执行流程

5.Filter与Interceptor:

接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandleaInterceptor接口。

拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只 会拦截Spring环境中的资源。

3、登录校验案例

4、全局异常处理器

编写一个exception包,其中包含GlobalExceptionHandler类,相关方法实现如下:

1
2
3
4
5
6
7
8
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result ex(Exception ex) {
ex.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
}

七、事务管理

1、概念

事务是一组操作的集合,它是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败。

操作:

开启事务(- -组操作开始前,开启事务) : start transaction / begin ;

提交事务(这组操作全部成功后,提交事务) : commit ;

回滚事务(中间任何-一个操作出现异常,回滚事务) : rollback ;

2、Spring事物管理

案例分析:删除部门的时候,删除部门下的全部员工。

会出现的问题:即使程序运行抛出了异常 / 部门依然删除了 / 但是部门下的员工却没有删除 , 造成了数据的不一致。

@Transactional

注解: @Transactional

位置:业务(service) 层的方法上、类上、接口上

作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务

作用在接口/类,在调用此接口/类的,都会被执行。

一般都在增删改的方法上加此注解。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    @Transactional
//加上@Transactional后整个方法将变成事务(Spring事务管理)
@Override
public void delete(Integer id) throws Exception {
try {
deptMapper.deleteById(id); //根据ID删除部门数据

int i = 1/0;
//if(true){throw new Exception("出错啦...");}

empMapper.deleteByDeptId(id); //根据部门ID删除该部门下的员工
} finally {
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次解散的是"+id+"号部门");
deptLogService.insert(deptLog);
}
}

事务进阶

rollbackFor

默认情况下,只有出现RuntimeException才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务 。

设置回滚异常类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Transactional(rollbackFor = Exception.class)
@Override
public void delete(Integer id) throws Exception {
try {
deptMapper.deleteById(id); //根据ID删除部门数据

int i = 1/0;
//if(true){throw new Exception("出错啦...");}

empMapper.deleteByDeptId(id); //根据部门ID删除该部门下的员工
} finally {
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次解散的是"+id+"号部门");
deptLogService.insert(deptLog);
}
}
propagation

事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

属性值 含义
REQUIRED [默认值]需要事务,有则加入,无则创建新事务
REQUIRES_ NEW 需要新事务,无论有无,总是创建新事务
SUPPORTS 支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY 必须有事务,否则抛异常
NEVER 必须没事务,否则抛异常

使用try-finally语句,即使上方执行异常,下方仍然可以正常操作(保障日志记录正常):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Transactional
@Override
public void delete(Integer id) throws Exception {
try {
deptMapper.deleteById(id); //根据ID删除部门数据

int i = 1/0;
//if(true){throw new Exception("出错啦...");}

empMapper.deleteByDeptId(id); //根据部门ID删除该部门下的员工
} finally {
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次解散的是"+id+"号部门");
deptLogService.insert(deptLog);
}
}

如果只写一个@Transactional,如果出错,会导致日志写入后回滚,把日志删除

因此,在创建日志的记录中,使用@Transactional(propagation = Propagation.REQUIRES_NEW),使其开启一个新事务这样就不会没有日志了。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class DeptLogServiceImpl implements DeptLogService {

@Autowired
private DeptLogMapper deptLogMapper;

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}

注意:正在运行的事务只能有一个,原运行的事物会被挂起,而运行新事务。

REQUIRED:大部分情况下都是用该传播行为即可。

REQUIRES_ NEW:当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。

八、AOP

1、基础

AOP:Aspect Oriented Programming(面向切面编程面向方面编程),其实就是面向特定方法编程。

2、SpringAOP快速入门

导入依赖:在pom.xml中导入AOP的依赖

SpringAOP是基于动态代理技术实现的。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

编写AOP程序:对于特定方法根据业务需要进行编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@Component //交给AOP管理
//@Aspect //AOP类
public class TimeAspect {

//@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") //切入点表达式
@Around("com.itheima.aop.MyAspect1.pt()") //接入点表达式 //设置哪些方法需要统计耗时
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
//1. 记录开始时间
long begin = System.currentTimeMillis();

//2. 调用原始方法运行
Object result = joinPoint.proceed();

//3. 记录结束时间, 计算方法执行耗时
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature()+"方法执行耗时: {}ms", end-begin);

return result;
}
}

3、AOP基础

1.连接点:JoinPoint,可以被 AOP 控制的方法(暗含方法执行时的相关信息)

2.通知: Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

3.切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用

4.切面:Aspect,描述通知与切入点的对应关系(通知+切入点)

5.目标对象:Target,通知所应用的对象

4、AOP执行流程

5、AOP通知类型

  1. @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  2. @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  3. @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  4. @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  5. @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Slf4j
@Component
//@Aspect
public class MyAspect1 {

@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pt(){}
//该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。

@Before("pt()")
public void before(){
log.info("before ...");
}

@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");

//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();

log.info("around after ...");
return result;
}

@After("pt()")
public void after(){
log.info("after ...");
}

@AfterReturning("pt()")
public void afterReturning(){
log.info("afterReturning ...");
}

@AfterThrowing("pt()")
public void afterThrowing(){
log.info("afterThrowing ...");
}
}

6、AOP通知顺序

1.不同切面类中,默认按照切面类的类名字母排序:

目标方法前的通知方法:字母排名靠前的先执行

目标方法后的通知方法:字母排名靠前的后执菏

2.用@Order(数字)加在切面类上来控制顺序

令目标方法前的通知方法:数字小的先执行

目标方法后的通知方法:数字小的后执行

7、切入点表达式

切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式 :

  1. execution (…) :根据方法的签名来匹配
  2. @annotationc(…) :根据注解匹配

1.切入点表达式-execution(最常用)

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

其中带?的表示可以省略的部分

  • 访问修饰符:可省略(比如: public、 protected)
  • 包名.类名:可省略
  • throws异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

常用的切入点表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
//@Aspect
@Component
public class MyAspect7 {

//匹配DeptServiceImpl中的 list() 和 delete(Integer id)方法
@Pointcut("execution(* com.itheima.service.DeptService.list()) || execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
private void pt(){}

@Before("pt()")
public void before(){
log.info("MyAspect7 ... before ...");
}
}

可以使用通配符描述切入点:

单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

1
execution(* com.*. service.*.update*(*))

多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

1
execution(* com.itheima..DeptService.*(..))

注意事项

根据业务需要,可以使用且(&&)、或(|)、非(!)来组合比较复杂的切入点表达式。

2.切入点表达式-@annotation(多一步,但灵活)

@annotation 切入点表达式,用于匹配标识有特定注解的方法。

1
@annotation(com.itheima.anno.Log)

(1)编写一个注解

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}

(2)将方法添加注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
//@Aspect
@Component
public class MyAspect7 {
//匹配DeptServiceImpl中的 list() 和 delete(Integer id)方法
//@Pointcut("execution(* com.itheima.service.DeptService.list()) || execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
@Pointcut("@annotation(com.itheima.aop.MyLog)")
//替换成@annotation
private void pt(){}

@Before("pt()")
public void before(){
log.info("MyAspect7 ... before ...");
}
}

8、连接点及其相关信息

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

对于@Around 通知,获取连接点信息只能使用ProceedingJoinPoint

对于其他四种通知,获取连接点信息只能使用JoinPoilt,它是ProceedingJoinPoint的父类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Slf4j
@Aspect
@Component
public class MyAspect8 {

@Pointcut("execution(* com.itheima.service.DeptService.*(..))")
private void pt(){}

@Before("pt()")
// (JoinPoint joinPoint)获取连接点信息
public void before(JoinPoint joinPoint){
log.info("MyAspect8 ... before ...");
}

@Around("pt()")
// (ProceedingJoinPoint joinPoint)获取连接点信息
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("MyAspect8 around before ...");

//1. 获取 目标对象的类名 .
String className = joinPoint.getTarget().getClass().getName();
log.info("目标对象的类名:{}", className);

//2. 获取 目标方法的方法名 .
String methodName = joinPoint.getSignature().getName();
log.info("目标方法的方法名: {}",methodName);

//3. 获取 目标方法运行时传入的参数 .
Object[] args = joinPoint.getArgs();
log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));

//4. 放行 目标方法执行 .
Object result = joinPoint.proceed();

//5. 获取 目标方法运行的返回值 .
log.info("目标方法运行的返回值: {}",result);

log.info("MyAspect8 around after ...");
return result;
}
}

9、AOP案例

【稍后更新】

九、Bean管理

1、配置优先级

案例:

同一个属性,不同地方配置3次,最终哪一个生效

优先级:

1.application.properties

2.application.yml

3.application.yaml

注意事项:

虽然SpringBoot支持多种格式配置文件,但是在项目开发时,推荐统一使用一 种格式的配置(yml是主流)。

配置:

SpringBoot 除了支持配置文件属性配置,还支持Java系统属性和命令行参数的方式进行属性配置。(按顺序如下)

1.命令行参数

1
--server.port=10010

2.Java系统属性

1
-Dserver.port=9000

①执行maven打包指令package

tlias-web-management-0.0.1-SNAPSHOT.jar

②.执行java指令,运行jar包

1
java -Dserver.port=9000 -jar tlias-web- management-0.0.1-SNAPSHOT.jar --server.port=10010

注意事项:

Springboot项目进行打包时,需要引入插件spring-boot -maven-plugin (基于官网骨架创建项目,会自动添加该插件)

优先级:

2、Bean 管理

1.获取bean

默认情况下,Spring项目启动时, 会把bean都创建好放在I0C容器中,如果想要主动获取这些bean,可以通过如下

方式:

根据name获取bean:

1
Object getBean (String name)

根据类型获取bean:

1
<T> T getBean (Class<T> requiredType)

根据name获取bean (带类型转换):

1
<T> T getBean(String name, Class<T> requiredType)

注意事项

上述所说的【Spring项目启动时,会把其中的bean都创建好】还会受到作用域及延迟初始化影响,这里主要针对于默认的单例非延迟加载的bean而言。

2.Bean作用域

作用域 说明
singleton* 容器内同名称的bean 只有一个实例(单例)(默认)
prototype* 每次使用该bean时会创建新的实例(非单例)
request 每个请求范围内会创建新的实例(web环境中,了解)
session 每个会话范围内会创建新的实例(web环境中,了解)
application 每个应用范围内会创建新的实例(web环境中,了解)

可以用@Scope注解来进行配置作用域:

1
2
3
4
5
@Scope("prototype")
@RestController
@RequestMapping("/depts")
public class DeptController {
}

注意事项:

  • 默认singleton的bean,在容器启动时被创建,可以使用@Lazy注解来延迟初始化( 延迟到第一次使用时) 。
  • prototype的bean,每一次使 用该bean的时候都会创建一个新的实例。
  • 实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性。

3、第三方Bean

第三方bean用:@Bean

如果要管理的bean对象来自于第三方(不是自定义的),是无法用@Component及衍生注解声明bean的,就需要用到@Bean注解。

直接定义到启动类当中(不建议):

1
2
3
4
5
6
7
@SpringBootApplication
public class SpringbootWebConfig2App1ication {
@Bean//将方法返回值交给IOC容器管理,成为IOC容器的bean对象
public SAXReader saxReader(){
return new SAXReader();
}
}

配置到方法中:(加注释)

1
2
3
4
5
6
7
@Configuration//配置类
public class CommonConfig {
@Bean
public SAXReader saxReader(){
return new SAXReader() ;
}
}

@Bean注释:

将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean

通过@Bean注解的name/value属性指定bean名称, 如果未指定, 默认是方法名

1
2
3
4
5
6
@Test
public void testGetBean2(){
Object saxReader = applicationContext.getBean("reader");
//""内,默认是方法名
System.out.println(saxReader);
}

需要依赖其他Bean对象,直接在方法中定义形参,容器会自动装配

1
2
3
4
5
6
7
8
9
10
@Configuration //配置类
public class CommonConfig {
//声明第三方bean
@Bean //将当前方法的返回值对象交给IOC容器管理, 成为IOC容器bean
//通过@Bean注解的name/value属性指定bean名称, 如果未指定, 默认是方法名
public SAXReader reader(DeptService deptService){
System.out.println(deptService);
return new SAXReader();
}
}

总结:

通过@Bean注解的name或value属性’ 可以声明bean的名称,如果不指定,默认bean的名称就是方法名。

如果第三方bean需要依赖其它bean对象,直接在bean定义方法中设置形参即可,容器会根据类型自动装配。

问题思考:

@Component及衍生注解与@Bean注解使用场景?

项目中自定义的,使用@Component及其衍生注解

项目中引入第三方的,使用@Bean注解

十、SpringBoot原理

1、Spring

Spring Framework框架:相对繁琐

Spring Boot框架:相对方便

原因:起步依赖(依赖传递)、自动配置

2、自动配置

SpringBoot的自动配置就是当Spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。

自动配置原理

方案一: @ComponentScan 组件扫描(使用繁琐,性能低)

包扫描范围:

1
2
3
4
5
6
7
8
@ComponentScan({"com.example","com.itheima"}) //设置包扫描范围
@SpringBootApplication //默认只扫描当前包及其子包
public class SpringbootWebConfig2Application {

public static void main(String[] args) {
SpringApplication.run(SpringbootWebConfig2Application.class, args);
}
}

方案二:@lmport 导入。使用@Import导入的类会被Spring加载到IOC容器中,导入形式主要有以下几种:

  • 导入普通类
  • 导入配置类
  • 导入ImportSelector接口实现类
  • @EnableXxxx注解,封装@Import注解(最方便)
1
2
3
4
@Import({TokenParser.class, HeaderConfig.class})
@SpringBootApplication
public class SpringbootWebConfig2Application {
}

3、@Conditional

作用:按照- -定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring I0C容器中。

位置:方法、类

  • @Conditional本身是一个父注解,派生出大量的子注解:
  • @ConditionalOnClass:判断环境中是否有对应字节码文件,才注册bean到IOC容器。
  • @ConditionalOnMissingBean:判断环境中没有对应的bean (类型或名称),才注册bean到IOC容器。
  • @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。

举例:

1
2
3
4
5
6
7
8
9
10
    @Bean
@ConditionalOnClass(name = "io.jsonwebtoken.Jwts") //环境中存在指定的这个类,才会将该bean加入IOC容器

//@ConditionalonClass (name = "io.jsonwebtoken.Jwts") //环境中存在指定的这个类,才会将该bean加入IOC容器中
//@ConditionalOnMissingBean // 不存在该类型的bean,才松将该bean加入I0C容器中 ---指定类型(value属性) 或名称(name属性)
//@ConditionalonProperty(name = "name", havingValue = "itheima") //配置文件中存在指定的属性与值,才会将该bean加入IOC容器外

public HeaderParser headerParser(){
return new HeaderParser();
}

全部注册为IOC容器的bean??? NO!

SpringBoot会根据@Conditional注解条件装配

4、自定义starter

在实际开发中,经常会定义-些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot的starter。

案例:
需求:自定义aliyun-oss-spring-boot-starter,完成阿里云OSS操作工具类AliyunOSSUtils 的自动配置。
目标:引入起步依赖引入之后,要想使用阿里云OSS,注入AliyunOSSUtils直接使用即可。

十一、Maven高级


JavaWeb基础2-2
https://davidpenn888.github.io/2023/05/30/JavaWeb_2_2/
作者
David Penn
发布于
2023年5月30日
许可协议