目录
Spring 在ssm中起什么作用? 什么是Spring? Spring的特点?
讲解一下核心容器(spring context应用上下文) 模块
Spring 在ssm中起什么作用? 什么是Spring? Spring的特点?
- Spring:轻量级开发框架。
- Spring解决了企业级应用开发的复杂性,简化Java开发。
- 作用:Bean工厂,用来管理Bean的生命周期和框架集成。
- 两大核心:
- IOC/DI(控制反转/依赖注入) :把dao依赖注入到service层,service层反转给action层,Spring顶层容器为BeanFactory。DI(依赖注入):全称为 Dependency Injection,意思自身对象中的内置对象是通过注入的方式进行创建。
- AOP:面向切面编程 AOP能够让我们在不影响方法原有功能的前提下,去增强方法。
IOC 和 DI 的关系:ioc 就是容器,di 就是注入这一行为,那么 di 确实就是 ioc 的具体功能的实现。而 ioc 则是 di 发挥的平台和空间。所以说,ioc 和 di 即是相辅相成的搭档,又是殊途同归的双胞胎。最重要的是,他们都是良好的降低耦合的思想。
在之前我们在contrller层中如果想要用service中的方法,我们需要把service给new出来,但是现在我们把创建对象的控制权交给spring管理。不需要自己去new 对象。在service层添加@Service注解,把sevice注入到spring容器中,然后再通过@Autowirld注解把service对象取出来使用。
aop能做什么?
可以做日志,权限,监控(记录每个方法的运行时长,看性能),拦截。
- 切面:拦截器类,其中会定义切点以及通知
- 切点:具体拦截的某个业务点。
- 通知:切面当中的方法,声明通知方法在目标业务层的执行位置,通知类型如下:
- 前置通知:@Before 在目标业务方法执行之前执行
- 后置通知:@After 在目标业务方法执行之后执行
- 返回通知:@AfterReturning 在目标业务方法返回结果之后执行
- 异常通知:@AfterThrowing 在目标业务方法抛出异常之后
- 环绕通知:@Around 功能强大,可代替以上四种通知,还可以控制目标业务方法是否执行以及何时执行
aop实际应用:
定义一个类。加上@Aspect和@Component注解。
类上面加上@Aspect表明这是一个切面类,@Component交给spirng管理。
在类中定义切点,@Pointcut注解,用execution表达式配置要拦截的方法(类),
然后就可以定义通知了,我一般喜欢用环绕通知@Round,它可以实现其他所有的功能。然后获取到方法名,然后就可以用method.getAnnotation(),获取到自定义的注解,我们就可以获取到注解中的参数,第一个参数是操作类型,第二个是具体的操作名称,操作人的话我们是用shiro做的登录,登录信息会保持在shiro中,我们通过TokenManager对象就可以获取到用户信息,然后就可以把信息添加到日志表中了.
JDK动态代理
原理:是根据类加载器和接口创建代理类(此代理类是接口的实现类,所以必须使用接口面向接口生成代理)
CGLIB动态代理
原理:将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
在Spring中。
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
spring aop 和Aspectj aop有什么区别
aop代理分为静态代理和动态代理
Aspectj就是静态代理
spring aop是动态代理
所谓静态代理,就是aop框架会在编译阶段生成aop代理类,因此也称为编译时增强。运行的时候就是增强之后的Aop对象。
spring aop使用的动态代理,就是说aop框架不会去修改字节码,而是每次运行时在内存临时为方法生成一个aop对象,在切点做了增强处理。
Spring的优缺点
优点
- 方便解耦,简化开发
Spring就是一个大工厂,可以将所有对象的创建和依赖关系的维护,交给Spring管理。 - AOP编程的支持
Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能。 - 声明式事务的支持
只需要通过配置就可以完成对事务的管理,而无需手动编程。 - 方便程序的测试12
Spring对Junit4支持,可以通过注解方便的测试Spring程序。 - 方便集成各种优秀框架
Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持(如:Struts、Hibernate、MyBatis等)。 - 降低JavaEE API的使用难度
Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低。
缺点
- Spring明明一个很轻量级的框架,却给人感觉大而全
- Spring依赖反射,反射影响性能
- 使用门槛升高,入门Spring需要较长时间
Bean的生命周期
1.实例化 由BeanFactory读取Bean定义文件,并生成各个实例
2.属性注入 Setter注入,执行Bean的属性依赖注入
3.初始化 Bean定义文件中定义init-method 初始化时执行
4.销毁 Bean定义文件中定义destroy-method,在容器关闭时执行
Spring 框架中都用到了哪些设计模式?
- 工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
- 单例模式:Bean默认为单例模式。
- 代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
- 模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
- 观察者模式:当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring在创建WebApplicationContext时,是通过一个ContextLoaderListener监听器实现的。监听器就是观察者模式的具体体现。
- 策略模式:在Spring中,我们可以使用JdbcTemplate实现对数据库的CRUD操作,而在查询时我们可能会用到RowMapper接口以及spring提供的一个BeanPropertyRowMapper的实现类。RowMapper接口就是规范,而我们根据实际业务需求编写的每个实现类,都是一个达成目标的策略。
- 适配器模式:在spring-framework中提供了springmvc的开发包。我们在用springmvc中,它实现控制器方式有很多,例如我们常用的使用@Controller注解,还有实现Controller接口或者实现HttpRequestHandler接口等等。而在DispatcherServlet中如何处理这三种不同的控制器呢,它用到了适配器,用于对不同的实现方式适配。
讲解一下核心容器(spring context应用上下文) 模块
这是基本的Spring模块,提供spring 框架的基础功能,BeanFactory 是任何以spring为基础的应用的核心。Spring 框架建立在此模块之上,它使Spring成为一个容器。
Bean 工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从真正的应用代码中分离。最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory ,它根据XML文件中的定义加载beans。该容器从XML 文件读取配置元数据并用它去创建一个完全配置的系统或应用。
Spring 是如何管理事务的,事务管理机制?
Spring 的事务机制包括声明式事务和编程式事务。
编程式事务管理:Spring 推荐使用 TransactionTemplate,实际开发中使用声明式事务较多。
声明式事务管理:直接在类或方法上加@Transactional注解。事务提交、回滚、异常处理等这些操作都不用我们处理了,Spring 都会帮我们处理。声明式事务管理使用了 AOP 面向切面编程实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。
spring事务有7种传播行为
1. 必须 表示该方法必须运行在一个事务中,否则将抛出异常。
2. 必须不 表示该方法不能在事务中运行,否则将抛出一个异常。
3. 嵌套 表示该方法应当运行在一个嵌套式事务中,该事务可以独立进行提交或回滚。
4. 不支持 表示该方法不应该在事务中运行,否则该事务会被挂起
5. 支持 表示该方法不需要事务,但如果有一个事务已经在运行,该方法也可以在这个事务中运行
6. 要求 表示该方法必须在一个事务中运行,如果一个现有事务正在进行中,该方法将在那个事务中运行,否则就要开始一个新事务。
7. 要求新建 表示该方法必须在它自己的事务中运行,一个新的事务将被启动。
Spring 中 Bean 的作用域有哪些?
作用域限定了 Spring Bean 的作用范围,在 Spring 配置文件定义 Bean 时,通过声明 scope 配置项,可以灵活定义 Bean 的作用范围。 scope 配置项有 5 个属性,用于描述不同的作用域。
① singleton:使用该属性定义 Bean 时,IOC 容器仅创建一个 Bean 实例,IOC 容器每次返回的是同一个 Bean实例。
② prototype:使用该属性定义 Bean 时,IOC 容器可以创建多个 Bean 实例,每次返回的都是一个新的实例。
③ request:该属性仅对 HTTP 请求产生作用,使用该属性定义 Bean 时,每次 HTTP 请求都会创建一个新的 Bean,适用于 WebApplicationContext 环境。
④ session:该属性仅用于 HTTP Session,同一个 Session 共享一个 Bean 实例。不同Session 使用不同的实例。
⑤ global-session: 全局Sission, 该属性仅用于 HTTP Session,同 session 作用域不同的是,所有的 Session 共享一个 Bean 实例。
Spring的单例非线程安全解决方案:
- 将这个bean的scope属性修改为prototype;
- 通过参数传递,使全局变量变成局部变量 ;
- 使用ThreadLocal修饰全局变量;
Spring 框架实现实例化的三种方式?
实例化bean方式
用构造器来实例化 <bean id="XXXbean" class=""/>
使用静态工厂方法实例化 <bean id="XXXbean" class="XXXfactoryClass" factory-method=""/>
使用实例工厂方法实例化<bean id="XXXbean" factory-bean="XXXfacotry" factory-method=""/>
第一种:使用构造器实例化 Bean:这是最简单的方式,Spring IoC 容器即能使用默认空构造器也能使用有参数构造器两种方式创建 Bean。
第二种:使用静态工厂方式实例化 Bean,使用这种方式除了指定必须的 class 属性,还要指定 factory-method 属性来指定实例化 Bean 的方法,而且使用静态工厂方法也允许指定方法参数,spring IoC 容器将调用此属性指定的方法来获取 Bean。
第三种:使用实例工厂方法实例化 Bean,使用这种方式不能指定 class 属性,此时必须使用 factory-bean 属性来指定工厂 Bean,factory-method 属性指定实例化 Bean 的方法,而且使用实例工厂方法允许指定方法参数,方式和使用构造器方式一样。
现在我们基本上都是用SpringBoot,约定大于配置,我们直接定义一个方法,返回一个bean实例,在方法上加上@Bean注解,就定义了一个Bean,我们想使用的话直接用autowried去注入就行了。
spring 框架实现依赖注入的方式是什么?
平时使用的是基于注解的依赖注入
实例化的注解列表:
@Compoment :应用于普通的类中
@Service:在servie层中使用
@Controller:标记controller层
@Reposistory:在dao层中使用
@Bean:自定义一个Bean的实例
注入的注解的标签:
@Autowired:自动注入的方式进行注入,默认根据类的名称来进行装配
@Resource(name="") 没有name时3和autowired一样但是他可以根据名称进行装配.
依赖注入是 Spring 协调不同 Bean 实例之间的合作而提供的一种工作机制,在确保 Bean实例之间合作的同时,并能保持每个 Bean 的相对独立性。
- 基于构造函数的依赖注入 构造函数注入就是通过 Bean 类的构造方法,将 Bean 所依赖的对象注入。构造函数的参数一般情况下就是依赖项,spring 容器会根据 bean 中指定的构造函数参数来决定调用那个构造.
- 基于设置函数的依赖注入 将 Bean 所依赖的对象通过设置函数注入,Bean 需要为注入的依赖对象提供设置方法。
- 基于自动装配的依赖注入 IOC 容器会基于反射查看 Bean 定义的类。当 Spring 容器发现 Bean 被设置为自动装配的 byType 模式后,它会根据参数类型在 Spring 容器中查找与参数类型相同的被依赖Bean 对象,如果已经创建,则会把被依赖的对象自动注入到 Bean 中,如果没有创建,则不会注入。注入过程需要借助 Bean 提供的设置方法来完成,否则注入失败。
Spring 常用注解
@Component, @Controller, @Repository, @Service.@Configuraction
@Component: 这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。
@Valid springboot参数校验器
Spring是怎么解决循环依赖的?
1、构造器注入形成的循环依赖。也就是beanB需要在beanA的构造函数中完成初始化,beanA也需要在beanB的构造函数中完成舒适化,这种情况循环依赖难以解决。可以在构造方法上加@Lazy注解,此时代表构造方法中的B是延时加载,所以构造的时候Spring先帮我们创建了B的代理对象,这时候A就可以实例化成功,此时再去实例化B,因为A已经实例化完成,那么B也可以实例化成功了。
2、setter注入构成的循环依赖。beanA需要在beanB的setter方法中完成初始化,beanB也需要在beanA的setter方法中完成初始化,spring设计的机制主要就是解决这种循环依赖。
解决步骤:
首先我们要知道什么是Spring的循环依赖问题。假如我们有两个bean,A 和 B。他们的代码简单如下:
@Bean
public class A {
@Autowire
private B b;
}
@Bean
public class B {
@Autowire
private A a;
}
也就是需要在A中注入B,在B中注入A,那么Spring在创建A的时候会出现这种现象:创建A实例后,在依赖注入时需要B,然后就去创建B,这时候发现又需要依赖注入 A ,这样就导致了循环依赖。
Spring对循环依赖的解决方法可以概括为 用三级缓存方式达到Bean提前曝光的目的
过程:A进行初始化,发现需要注入B,于是先将自己的ObjectFactory方法存到三级缓存中,从而使其他bean能引用到该beanA,然后去初始化B,初始化B的时候发现需要注入A,于是又回过头去初始化A,这时候程序发现原来的beanA正处在初始化状态,那么就会依次从一级缓存,二级缓存,三级缓存去找,最终在三级缓存找到刚才A提前存进去的ObjectFactory方法,然后保存到二级缓存中,删掉三级缓存的数据。然后B拿到A的引用之后完成了自己的初始化的过程,接着将B存放到一级缓存(单例缓存)中,删掉二级缓存和三级缓存的数据。接下来A就可以在一级缓存拿到B的引用,从而完成A自身的初始化。
为什么一定要三级缓存,二级缓存不够吗?
因为在三级缓存取数据的时候,会先判断对象需不需要代理,需要的话创建bean的代理并返回,不需要的话则直接返回 bean。如果有一个类加上@Transactional注解的话,肯定是要进行AOP代理的,没有三级缓存的话就没法生成bean的代理类。
如果一个接口有多个实现类,怎么确定注入的是哪个实现类?
通过@Resource注解,并指定name属性为要注入的实现类名,就可以指定。
因为@Resource是根据名称注入,@AutoWired是通过类型注入。
或者用 @Autowired 和 @Qualifier 来指定。
接口的幂等性
什么是接口的幂等性?
现在我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,既然是通信,那么就有可能在服务器处理完毕后返回结果的时候挂掉,这个时候用户端发现很久没有反应,那么就会多次点击按钮,这样请求有多次,那么处理数据的结果是否要统一呢?那是肯定的!尤其在支付场景。
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性。
什么情况下需要保证接口的幂等性
在增删改查4个操作中,尤为注意就是增加或者修改,
A: 查询操作
查询对于结果是不会有改变的,查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作。
B: 删除操作
删除一次和多次删除都是把数据删除。=在不考虑返回结果的情况下,删除操作也是具有幂等性的
C: 更新操作
修改在大多场景下结果一样,但是如果是增量修改是需要保证幂等性的,如下例子:
把表中id为XXX的记录的A字段值设置为1,这种操作不管执行多少次都是幂等的,
把表中id为XXX的记录的A字段值增加1,这种操作就不是幂等的。
D: 新增操作
增加在重复提交的场景下会出现幂等性问题,如以上的支付问题。
如何设计接口才能做到幂等呢?
使用token机制实现
下面以支付系统为例,分别对接口的幂等性进行说明与实现
使用token机制是通用性强的实现方法,我们只探讨这种方式
token机制实现步骤:
1. 生成全局唯一的token,token放到redis中,token会在页面跳转时获取.存放到pageScope中,支付请求提交先获取token
2. 提交后后台校验token,执行提交逻辑,提交成功同时删除token,生成新的token更新redis ,这样当第一次提交后token更新了,页面再次提交携带的token是已删除的token后台验证会失败不让提交
token特点: 要申请,一次有效性,可以限流
注意: redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用
核心思想:Token机制 + redis + AOP
实现过程:前端页面跳转时就请求服务器生成一个token,token必须是全局唯一,可以用用户id+时间戳,把token保存到redis中,设置过期时间。然后服务器返回token,放在前端页面page中,当前端表单提交时,携带页面token一起发给服务器,服务器判断token和redis中保存的token是否一致,如果一致,说明是一次请求,放行,执行流程,执行完后删除redis,生成新token,更新redis的token。返回新token。
如果服务器判断前端传的token和redis中的token不一致,就说明是重复提交,拒绝流程。如果前端传的token是空,或者redis中的token是空,也直接拒绝。
Token机制,防止页面重复提交
业务要求:页面的数据只能被点击提交一次
发生原因:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交
解决办法:
集群环境:采用 token 加 redis(redis 单线程的,处理需要排队)
单 JVM 环境:采用 token 加 redis 或 token 加 jvm 内存
处理流程:
数据提交前要向服务的申请 token,token 放到 redis 或 jvm 内存,token 有效时间
提交后后台校验 token,同时删除 token,生成新的 token 返回
token 特点:要申请,一次有效性,可以限流
基于Token方式防止API接口幂等
客户端每次在调用接口的时候,需要在请求头中,传递令牌参数,每次令牌只能用一次。
一旦使用之后,就会被删除,这样可以有效防止重复提交。
步骤:
1.生成令牌接口
- 接口中获取令牌验证
生成令牌接口
RedisTokenUtils工具类
public class RedisTokenUtils { private long timeout = 60 * 60; @Autowired private BaseRedisService baseRedisService; // 将token存入在redis public String getToken() { String token = "token" + System.currentTimeMillis(); baseRedisService.setString(token, token, timeout); return token; } public boolean findToken(String tokenKey) { String token = (String) baseRedisService.getString(tokenKey); if (StringUtils.isEmpty(token)) { return false; } // token 获取成功后 删除对应tokenMapstoken baseRedisService.delKey(token); return true; }}
@Target(value = ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface ExtApiIdempotent { String value();}
@Target(value = ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface ExtApiToken {}
自定义Api幂等注解和切面
@Componentpublic class ExtApiAopIdempotent { @Autowired private RedisTokenUtils redisTokenUtils; @Pointcut("execution(public * com.itmayiedu.controller.*.*(..))") public void rlAop() { } // 前置通知转发Token参数 @Before("rlAop()") public void before(JoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class); if (extApiToken != null) { extApiToken(); } } // 环绕通知验证参数 @Around("rlAop()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature(); ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class); if (extApiIdempotent != null) { return extApiIdempotent(proceedingJoinPoint, signature); } // 放行 Object proceed = proceedingJoinPoint.proceed(); return proceed; } // 验证Token public Object extApiIdempotent(ProceedingJoinPoint proceedingJoinPoint, MethodSignature signature) throws Throwable { ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class); if (extApiIdempotent == null) { // 直接执行程序 Object proceed = proceedingJoinPoint.proceed(); return proceed; } // 代码步骤: // 1.获取令牌 存放在请求头中 HttpServletRequest request = getRequest(); String valueType = extApiIdempotent.value(); if (StringUtils.isEmpty(valueType)) { response("参数错误!"); return null; } String token = null; if (valueType.equals(ConstantUtils.EXTAPIHEAD)) { token = request.getHeader("token"); } else { token = request.getParameter("token"); } // 2.判断令牌是否在缓存中有对应的令牌 // 3.如何缓存没有该令牌的话,直接报错(请勿重复提交) // 4.如何缓存有该令牌的话,直接执行该业务逻辑 // 5.执行完业务逻辑之后,直接删除该令牌。 if (StringUtils.isEmpty(token)) { response("参数错误!"); return null; } if (!redisTokenUtils.findToken(token)) { response("请勿重复提交!"); return null; } Object proceed = proceedingJoinPoint.proceed(); return proceed; } public void extApiToken() { String token = redisTokenUtils.getToken(); getRequest().setAttribute("token", token); } public HttpServletRequest getRequest() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); return request; } public void response(String msg) throws IOException { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletResponse response = attributes.getResponse(); response.setHeader("Content-type", "text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); try { writer.println(msg); } catch (Exception e) { } finally { writer.close(); } }}
页面防止重复提交
public class OrderPageController { @Autowired private OrderMapper orderMapper; @RequestMapping("/indexPage") @ExtApiToken public String indexPage(HttpServletRequest req) { return "indexPage"; } @RequestMapping("/addOrderPage") @ExtApiIdempotent(value = ConstantUtils.EXTAPIFROM) public String addOrder(OrderEntity orderEntity) { int addOrder = orderMapper.addOrder(orderEntity); return addOrder > 0 ? "success" : "fail"; }}
spring中的bean线程安全吗
不安全,bean默认是单例的,spring没有进行多线程的封装处理。
怎么解决,最简单的方法就是改变bean的作用域,比如@Service注解,下面加个@Scope("prototype")
把单例改成多例,这样请求bean就相当于new bean(),就可以保证线程安全了。
尽量不要使用全局变量,可以通过方法传参
当一个应用启动很慢怎么优化
1.查看部署应用系统的系统资源使用情况,cpu内存,io这几个方面去看,找到对应的进程
2.查看GC日志查看是哪段代码在占用内存,调节内存的参数设置
3.定位代码,修改代码。一般是代码获取的数据量过大