spring 用法总结
三种向容器注入 bean 的方式
无参构造实例化
使用 @Configuration 注解标识 UserServiceConfig 是一个配置类,并且在 userService 方法上用 @Bean 标识方法返回的对象需要注入到容器中,其中缺省的 bean id 就是方法名即 userSerivce,当然也可以使用 @Bean(“userSerivce”) 显式的指定bean id。
package org.example.config;
import org.example.beans.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
public class UserServiceConfig {
// Environment 在容器中是一个抽象的集合,是指应用环境的2个方面:profiles 和 properties。
@Autowired
private Environment environment;
@Bean
public UserService userService() {
// 从 application.yml 中获取端口号配置
String property = environment.getProperty("server.port");
return new UserService(Integer.parseInt(property));
}
}
工厂实例方法实例化
首先实现 FactoryBean 这个接口,标识这个实现类是一个 bean 工厂类,注意一点,我们实现的这个 bean 工厂也是一个 bean,这个 bean 也是需要被 spring 容器管理的,所以要加上 @Component 注解;然后,实现的两个方法看名字也知道是啥意思了,就不多解释,其实接口里还有一个 isSingleton() 方法是默认返回 true 的,意思就是实例化的 bean 默认是单例的。好了,这样就可以了,容器里已经可以拿到我们定义的 bean 了。
package org.example.beans;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
public class UserServiceDynamicFactory implements FactoryBean<UserService> {
@Autowired
private Environment environment;
private UserService getInstance() {
return new UserService(Integer.parseInt(environment.getProperty("server.port")));
}
// 获取实例
@Override
public UserService getObject() {
return getInstance();
}
// 实例类型
@Override
public Class<?> getObjectType() {
return UserService.class;
}
}
工厂静态方法实例化
工厂静态方法实例化这种使用场景很少,因为工厂实例方法就可以满足大部分使用需求了,目前我还是没发现工厂静态方法实例化的特殊使用场景。需要提一下跟工厂实例方法实例化不同的地方,它不用实现 FactoryBean 接口,直接写一个工厂返回实例,需要注意的是返回实例的方法是静态方法。这种方法用的很少,细心点的可以发现我前两种注入的 bean 都是给实例变量赋了值的,值为配置文件中的配置项,如果我用工厂静态方法实例化的话就无法使用 @Autowired 注入其他组件,那么就得手动获取 spring 上下文,然后从上下文里获取,这种方式就特别麻烦,所以用的很少。
package org.example.beans;
import org.springframework.context.annotation.Bean;
@Configuration
public class UserServiceStaticFactory {
// 注意方法修饰关键字是 static
@Bean
public static UserService getInstance() {
return new UserService();
}
}
三种注解
@Autowired
按照类型注入
@Autowired
private Environment environment;
@Autowired + @Qualifier(“beanid”)
按照 bean id 注入
@Autowired
@Qualifier("environment")
private Environment environment;
@Resource(“beanid”)
等同于 @Autowired + @Qualifier(“beanid”)
@Resource("environment")
private Environment environment;
注解配置
@Configuration
标识当前类为配置类,相当于以前的applicationContext.xml、application-jdbc.xml、application-xxx.xml等。
@PropertySource(“classpath:xxx.properties”)
指定要读取的外部配置文件
@Import
这个作用于配置类上,作用等同于使用 xml 配置的 <import reousrce = “application-xxx.xml”/> 标签,这个适用场景就是当工程里有多个配置模块,的时候可以指定一个主配置,然后,在主配置里统一引入其他模块的配置。记住这个注解,很好用。
package org.example.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import javax.sql.DataSource;
// 标志该类是一个配置类,作用等同于 applicationContext.xml 的配置
@Configuration
// <context:component-scan base-package="org.example"/> 作用等同于 @ComponentScan 注解,可以定义扫描 base-package
//@ComponentScan({"org.example.config", "org.example.beans"})
// <context:property-placeholer location="classpath:jdbc.properties"/> 作用等同于 @PropertySource 可以指定外部配置文件
@PropertySource("classpath:jdbc.properties")
// <import resource=""/> 作用等同于 @Import(XXX.class),可以引入其他配置模块
@Import(OtherConfiguration.class)
/**
1. jdbc.driver=com.mysql.cj.jdbc.Driver
2. jdbc.url=jdbc:mysql://localhost:3306/funtower?serverTimezone=UTC&useSSL=true
3. jdbc.username=root
4. jdbc.password=123456
*/
public class DataSourceConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
两个重要的上下文
ClassPathXmlApplicationContext
类加载路径下的xml配置的上下文
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
AnnotationConfigApplicationContext
注解配置的上下文
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
spring 集成单元测试
- 导入 spring 集成 Junit 的坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.15.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.15.RELEASE</version>
</dependency>
- 使用 @Runwith 注解替换原来的运行期
- 使用 @ContextConfiguration 指定配置文件或配置类
- 使用 @Autowired 注入需要测试的对象
- 创建测试方法进行测试
package com.kilobytech;
import org.example.config.DataSourceConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
// 指定 spring 集成的 Junit 测试类
@RunWith(SpringJUnit4ClassRunner.class)
//@ContextConfiguration("classpath:applicationContext.xml") 指定 xml 配置的位置
// 同样也可以使用以下方法指定配置类
@ContextConfiguration(classes = DataSourceConfig.class)
public class SpringTest {
// 在测试类里就可以直接注入 bean
@Autowired
private DataSource dataSource;
@Test
public void testDataSource() throws SQLException {
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
}
}
spring AOP
前置基础
jdk 动态代理
jdk 动态代理是基于接口实现的,代理目标和代理对象必须实现同一个接口,看代码:
首先声明一个接口 Target
package org.example.proxy.jdk;
/**
* jdk 动态代理对象需要实现的接口
*/
public interface Target {
void save();
}
然后,写一个实现类,模拟被代理的目标
package org.example.proxy.jdk;
/**
* 代理目标类
*/
public class TargetImpl implements Target {
@Override
public void save() {
System.out.println("save running...");
throwException();// 模拟运行时出现异常
}
public void throwException() {
throw new RuntimeException("运行时异常!");
}
}
这时候就是常见的编码场景,一个实现类,一个接口。那么,如果现在有这么一个需求,要在这个 TargetImpl 运行钱打一行日志,运行后又打一行日志,简单的就是硬编码,如下
public void save() {
System.out.println("before...");
System.out.println("save running...");
System.out.println("after...");
throwException();// 模拟运行时出现异常
}
但是这样,我们思考两个问题噢,第一,如果这个类有很多方法都需要前后打日志,而且不仅这个类需要,好多别的类也有这个需求要前后打日志,是否可以考虑将这种前后打日志的代码复用起来?第二,前后两个日志或者类似的这种代码往往跟业务毛关系没有,放在这里明显就是增加业务代码的长度。所以基于以上两点,硬编码就明显不合适了。先看第一个问题,复用的话我们可以将前后打日志的这种代码单独抽象成一个类里面的两个方法,如下:
package org.example.proxy.jdk;
/**
* 增强类
*/
public class Advice {
// 前置增强
public void before() {
System.out.println("before...");
}
// 后置增强
public void after() {
System.out.println("after...");
}
}
那接下来看第二个问题,现在已经将打日志和业务代码分开,那么如何在运行时将打日志这种代码植入业务代码中呢?记住,这种在 运行时植入,而非 编译期植入 的思想就是代理的本质。目标对象也有了,增强的方法也抽象出来了,怎么把它俩的关系搭建起来呢,接下里就是动态代理该干的事情了,首先明确一点,jdk 动态代理获取代理对象使用这个方法:
public class Proxy;
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h){
// proxy detail...
}
第一个参数就是类加载器,第二个参数就是目标对象实现的接口,也就是 Target,第三个就是调用处理器,这个调用处理器里有一个 invoke 方法需要实现,先看看这个方法:
public interface InvocationHandler;
Object invoke(Object proxy, Method method, Object[] args);
第一个参数就是被代理的对象 TargetImpl,第二个就是代理的方法,第三个就是方法入参,注意,既然我们可以拿到待执行的方法信息,那么就可以根据方法签名来决定是否代理当前方法,这种场景就像数据库连接池的连接对象除了 close() 以外,其他方法都不需要被代理,只有close() 需要特殊处理,大多数数据库连接池的 close() 方法都不是真正的关闭连接,而是被代理成归还连接池。那么看到这个方法,基本上就可以理解 jdk 动态代理的使用方法了,先创建一个 InvocationHandler,并实现 invoke 方法,在 invoke 方法里执行目标对象的方法,并在其执行前后植入增强方法,这样就实现了业务代码和日志代码分离,并且日志代码可以复用,具体代码如下:
package org.example.proxy.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyTest {
public static void main(String[] args) {
// 代理目标对象
final TargetImpl target = new TargetImpl();
// 增强类
Advice advice = new Advice();
// 返回值就是动态代理生成的对象
Target proxy = (Target) Proxy.newProxyInstance(
target.getClass().getClassLoader(),// 目标的对象类加载器
target.getClass().getInterfaces(),// 目标对象相同接口字节码对象数组
new InvocationHandler() {
// 调用代理对象的任何方法,实质执行的都是 invoke 方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强
advice.before();
Object invoke;
try {
invoke = method.invoke(target, args);// 执行目标方法
} catch (InvocationTargetException e) {
// InvocationTargetException 这个异常是程序被代理在运行时出现的错误,
// 要获取具体异常信息需要拿到它里面的 target 对象,
// 注意 InvocationTargetExceptiontarget 对象本身不是被代理目标的异常信息,它里面的 target 异常对象才是,
// 所以得通过 InvocationTargetException#getTargetException() 才能拿到运行时后的出错信息
invoke = null;
// 这个才是具体错误信息
Throwable targetException = e.getTargetException();
targetException.printStackTrace();
} catch (IllegalAccessException | IllegalArgumentException e) {
// 这两个异常就不多说了,见名知义,前者有可能就是方法没有访问权限,后者参数不合法
invoke = null;
} catch (Throwable e) {
// 其他错误
invoke = null;
}
// 后置增强
advice.after();
return invoke;
}
}
);
// 调用代理对象方法
proxy.save();
}
}
至于 jdk 动态代理的原理,这个网上应该有很多博客都讲得很明白了,所以这里就不赘述,简单说一下,jdk 动态代理就是先拿到目标对象所实现的接口,然后实现它,改造里面的方法,最后代理目标去执行被改造的方法。这里只把 jdk 动态代理源码里生成代理类的方法贴出来,有兴趣的伙计自己研究:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException {
Objects.requireNonNull(h);
// 获取代理目标所实现的接口
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
* 如果代理类字节码对象之前已经生成那么就复用,否则就生成
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
* 调用代理对象的构造器(这个构造器有一个参数就是 InvocationHandler)
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
// 生成一个代理对象,并将 InvocationHandler 传进去
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
cglib 动态代理
先引入 cglib maven 坐标
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
另外说明一点,spring5.x 是已经将 cglib 引入到了 spring-core 里,所以 sping5.x 可以不用显式引入 cglib maven 坐标,直接使用即可,和 jdk 动态代理一样,同样需要 Advice 和 target 目标,只不过不需要接口了,因为 cglib 是基于类继承方式实现的,所以先挪一下代码:
代理目标
package org.example.proxy.cglib;
/**
* 代理目标类
*/
public class Target {
public void save() {
System.out.println("save running...");
// throwException();// 模拟运行时出现异常
}
public void throwException() {
throw new RuntimeException("运行时异常!");
}
}
增强类
package org.example.proxy.cglib;
/**
* 增强类
*/
public class Advice {
// 前置增强
public void before() {
System.out.println("before...");
}
// 后置增强
public void after() {
System.out.println("after...");
}
}
cglib 动态代理也有一个类似 jdk 动态代理一样的调用处理器,只不过名字不一样,叫 Enhancer,下面贴出具体使用:
package org.example.proxy.cglib;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class ProxyTest {
public static void main(String[] args) {
// 代理目标对象
final Target target = new Target();
// 增强类
final Advice advice = new Advice();
// 返回值就是动态代理生成的对象 基于cglib
// 1.创建增强器
Enhancer enhancer = new Enhancer();
// 2.设置父类(目标)
enhancer.setSuperclass(Target.class);
// 3.设置回调
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object proxy, Method method, Object[] params, MethodProxy methodProxy) throws Throwable {
// 执行前置
advice.before();
// 执行目标
Object invoke;
try{
// 以下三种方法都可以,但是推荐使用 methodProxy.invokeSuper(proxy, params)
// 原理比较复杂,大致意思是 MethodProxy#invokeSuper 是基于缓存方法引用执行效率比直接反射调用 method.invoke 高
invoke = methodProxy.invokeSuper(proxy, params);
// invoke = method.invoke(target, params);
// invoke = methodProxy.invoke(target, params);
// methodProxy.invoke(proxy, params); 错误示范,会递归调用
} catch (Exception e) {
// Exception handling is the same as for JDK proxy
invoke = null;
}
// 执行后置
advice.after();
return invoke;
}
});
// 4.创建代理对象
Target proxy = (Target) enhancer.create();
proxy.save();
}
}
总结一下,jdk 动态代理和 cglib 动态代理都是通过生成字节码来对原目标类进行改造、加强设置重写被代理的方法,既然字节码都能生成了,那接下来就是将字节码通过类加载器将字节码加载到内存中,然后通过反射调用代理方法就可以达到对原目标进行增强的目的,另外它俩都有目标对象、代理对象、代理方法这三个概念。
spring AOP概念
spring 的 AOP 实现底层就是对上面的动态代理的代码进行封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。在正式讲解 AOP 之前有几个重要的概念术语需要解释清楚:
- Target(目标对象):代理的目标对象,参照上文所讲的目标对象
- Proxy(代理对象):一个类被 AOP 织入增强后的代理对象,参照上文所讲的代理对象
- Joinpoint(连接点):所谓的连接点说人话就是那些被拦截到的方法
- Pointcut(切入点):对连接点进行定义的点,也就是说一个类里有10个方法的话,那么这10个方法就可以理解为10个连接点,而其中有三个方法是需要被代理的,那么这三个方法就可理解为切入点,如果用集合论的概念理解那就是 Poincut ⊂ Jointpoint,任意 Pointcut 都属于 Joinpoint
- Advice(通知/增强):拦截到 Joinpot 之后要做的事情,参照上文的 Advice
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程,值得注意的是 spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入,参照上文提到的 编译期植入 和 运行期植入 的概念理解。
xml 配置 spring AOP
先引入 maven 依赖坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
切面对象
package org.example.aop;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAspect {
public void before() {
System.out.println("before running...");
}
public void afterReturning() {
System.out.println("afterReturning running...");
}
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕前...");
Object proceed = pjp.proceed();
System.out.println("环绕后...");
return proceed;
}
public void afterThrowing() {
System.out.println("异常抛出增强...");
}
public void after() {
System.out.println("最终增强");
}
}
目标对象
package org.example.aop;
public class Target {
public void save() {
System.out.println("save running...");
}
}
xml 配置
<!-- 目标对象 -->
<bean id="target" class="org.example.aop.Target"/>
<!-- 切面对象 -->
<bean id="myAspect" class="org.example.aop.MyAspect"/>
<!-- 配置织入:告诉 spring 哪些方法(切点)需要进行增强(前置、后置、环绕)-->
<aop:config>
<!-- 声明切面 -->
<aop:aspect ref="myAspect">
<!-- 切面:pointcut + Advice -->
<!-- <aop:before method="before" pointcut="execution(public void org.example.aop.Target.save())"/> -->
<aop:before method="before" pointcut="execution(* org.example.aop.*.*(..))"/>
<aop:after-returning method="afterReturning" pointcut="execution(* org.example.aop.*.*(..))"/>
<aop:around method="around" pointcut="execution(* org.example.aop.*.*(..))"/>
<aop:after-throwing method="afterThrowing" pointcut="execution(* org.example.aop.*.*(..))"/>
<aop:after method="after" pointcut="execution(* org.example.aop.*.*(..))"/>
</aop:aspect>
</aop:config>
spring test
@Resource
private Target target;
@Test
public void testAOP() {
target.save();
}
或者将切点单独抽出来
<aop:config>
<!-- 声明切面 -->
<aop:aspect ref="myAspect">
<!-- 声明切点 -->
<aop:pointcut id="myPointcut" expression="execution(* org.example.aop.*.*(..))"/>
<!-- 引用切点 -->
<aop:around method="around" pointcut-ref="myPointcut"/>
<aop:after method="after" pointcut-ref="myPointcut"/>
</aop:aspect>
</aop:config>
切点表达式的写法
表达式语法:
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
例如:"execution(public void org.example.aop.Target.save())"
- 访问修饰符可以省略
- 返回值类型、包名、类名、方法名可以使用 * 代表任意
- 包名与类名之间一个点 . 代表当前包下的类,两个点 .. 表示当前包及其子包下的类
- 参数列表可以适用两个点 .. 表示任意个数,任意类型的参数列表。
注解配置 spring AOP
先把 Target 和 MyAspect 挪过来,然后加上注解,代码如下所示:
Target
package org.example.aop;
import org.springframework.stereotype.Component;
@Component
public class Target {
public void save() {
System.out.println("save running...");
}
}
MyAspect
package org.example.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@Before("execution(* org.example.aop.Target.save(..))")
public void before() {
System.out.println("before running...");
}
@AfterReturning("execution(* org.example.aop.Target.save(..))")
public void afterReturning() {
System.out.println("afterReturning running...");
}
@Around("execution(* org.example.aop.Target.save(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕前...");
Object proceed = pjp.proceed();
System.out.println("环绕后...");
return proceed;
}
@AfterThrowing("execution(* org.example.aop.Target.save(..))")
public void afterThrowing() {
System.out.println("异常抛出增强...");
}
@After("execution(* org.example.aop.Target.save(..))")
public void after() {
System.out.println("最终增强");
}
}
最重要的是我们配置类,作用等同于 xml,就命名为 AopConfig 吧
package org.example.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("org.example.aop")
@EnableAspectJAutoProxy
public class AopConfig {
}
值得注意的是如果使用注解配置 AOP 的话,那么这个注解必须要加上 @EnableAspectJAutoProxy,否则 AOP 不生效,这个注解的效果也等同于在 xml 中做如下配置:
<!-- aop 自动代理 -->
<aop:aspectj-autoproxy/>
spring 事务
spring 事务三大对象
事务管理器
PlateformTransactionManager,它是一个事务管理器接口,由不同的数据访问层框架做具体实现,比如 MyBatis 和 Hibernate 的事务管理都实现了这个接口。
事务传播行为
其中 spring 提供一个对象来定义事务相关的行为,它就是 TransactionDefinition,以下是它的相关属性
- REQUIRED: 如果当前没有事务,就新建一个事务,如果已经存在一个事务,就加入到当前事务中。一般的选择(默认值)。
- SUPPORTS: 支持当前事务,如果当前没有事务,就以非事务的方式执行(没有事务)。
- MANDATORY: 使用当前事务,如果当前没有事务,就抛异常
- REQUERS_NEW: 新建事务,如果当前在事务中,把当前事务挂起。
- NOT_SUPPORTED: 以非事务的方式执行操作,如果当前存在事务,就把当前事务挂起。
- NEVER: 以非事务方式运行,如果当前存在事务,抛出异常。
- NESTED: 如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则执行 REQUIRED 类似的操作
- 超时时间: 默认值是 -1,没有超时限制,如果有,以秒为单位进行设置。
- 是否只读: 建议查询时设置为只读
事务状态
TransactionStatus 这个对象提供了事务的状态信息
基于 xml 的声明式事务控制
spring 的声明式事务顾名思义就是采用声明的方式来处理事务。这里说的声明,就是指在配置文件中声明,用在 spring 配置文件中声明式的处理事务来代替编程式处理事务。
- 声明式事务处理的作用就是降低对代码的侵入性
从 AOP 的角度理解这个声明式事务就业务类就是目标对象,方法就是切点,事务就是要增强的具体操做。
因为 xml 配置事务的格式比较固定,所以直接给出模板供各位伙计们参考:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 目标对象、内部方法就是切点 -->
<bean id="accountService" class="org.example.tx.service.AccountServiceImple"/>
<!-- 配置平台事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 通知 事务的增强 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 根据不同的方法(AOP 切点)设置事务的属性信息 -->
<tx:attributes>
<!-- <tx:method name="*" isolation="DEFAULT" propagation="REQUIRED" timeout="-1" read-only="false"/> -->
<tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" timeout="-1" read-only="false"/>
<tx:method name="findAll" isolation="DEFAULT" propagation="REQUIRED" timeout="-1" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 配置事务的 aop 织入 -->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* org.example.tx.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
<!-- 开启注解声明式事务支持,这个标签不能少噢,少了事务就不生效了 -->
<!-- <tx:annotation-driven/> -->
</beans>
基于注解的声明式事务控制
重点了解基于注解的声明式事务使用方法,原理等同于 xml 配置,只不过把 <bean id=“xxx” class=“xxx.xxx.XX”/> 变成了 @Service、@Repository、@Bean 等注解注入的方式,然后把 <tx:advice/> 和 <aop:config/> 标签变成了一个 @Transactional 注解,到这还没完,最后还是得在 xml 配置中使用 <tx:annotation-driven/> 这个标签,代表开启注解声明式事务支持,注意,这里还没有完全将 xml 配置消除噢,xml 中还留有这个标签 <tx:annotation-driven/>,完整代码如下:
新建一张表,sql 如下:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 账户表 就俩字段(账户名 + 金额)
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账户名',
`amt` int(0) NULL DEFAULT NULL COMMENT '金额'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- 插入两条数据
-- 账户:fun,金额:5000
-- 账户:tower,金额:5000
-- ----------------------------
INSERT INTO `account` VALUES ('fun', 5000);
INSERT INTO `account` VALUES ('tower', 5000);
SET FOREIGN_KEY_CHECKS = 1;
maven 坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
接下来编写 dao 层代码
package org.example.tx.dao;
public interface AccountDao {
// 账户收入
void in(String inName, Integer amt);
// 账户支出
void out(String outName, Integer amt);
}
package org.example.tx.dao;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
@Repository
public class AccountDaoImpl implements AccountDao {
@Resource
private JdbcTemplate jdbcTemplate;
@Override
public void in(String inName, Integer amt) {
jdbcTemplate.update("update account set amt = amt - ? where name = ?", amt, inName);
}
@Override
public void out(String outName, Integer amt) {
jdbcTemplate.update("update account set amt = amt + ? where name = ?", amt, outName);
}
}
编写 service 层代码
package org.example.tx.service;
public interface AccountService {
// 转账
void transfer();
}
package org.example.tx.service;
import org.example.tx.dao.AccountDao;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class AccountServiceImple implements AccountService {
@Resource
private AccountDao accountDao;
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public void transfer() {
accountDao.out("fun", 100);
// 模拟事务中抛出异常
int i = 1 / 0;
accountDao.in("tower", 100);
}
}
事务配置类
package org.example.config;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.sql.DataSource;
@Configuration
@ComponentScan("org.example.tx")
@Import({JdbcTemplateConfig.class, DataSourceConfig.class})
//@ImportResource("classpath:transaction.xml")
@EnableTransactionManagement// 等同于上面注释的 xml 中配置 <tx:annotation-driven/>
public class TransactionConfig {
@Resource
private DataSource dataSource;
@Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource);
}
}
测试
package com.kilobytech;
import org.example.config.TransactionConfig;
import org.example.tx.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
// 指定 spring 集成的 Junit 测试类
@RunWith(SpringJUnit4ClassRunner.class)
// 同样也可以使用以下方法指定配置类
@ContextConfiguration(classes = TransactionConfig.class)
public class TransactionTest {
@Resource
private AccountService accountService;
@Test
public void testAccount() {
accountService.transfer();
}
}
执行报错后看数据库的值是否没更新,若没更新,则代表事务生效,否则,就翻车了。。。
spring 集成 web
添加以下 maven 坐标
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.2.1</version>
<scope>provided</scope>
</dependency>
添加 web.xml 应用部署描述文件
在 web 项目中,可以使用 ServletContextListener 监听 Web 应用的启动,我们可以在 Web 应用启动时,就加载 spring 的配置创建 spring 上下文 ApplicationContext,在将其储存到最大的域 servletContext 域中,这样就可以在任何位置从域中获得 spring 上下文对象了,对了,对Java web 四大域不了解的可以看看这个链接。提一下,ModelAndView 中的属性值是存在 Request 域中的。
直接上代码:
package org.example.context;
import org.example.config.MainConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class ContextLoaderListener implements ServletContextListener {
/**
* 初始化 spring 上下文对象具体步骤:
* 1、创建 spring 上下文,ClassPathXmlApplicationContext 或者 AnnotationConfigApplicationContext
* 2、通过监听事件对象 ServletContextEvent 获取 ServletContext
* 3、把 spring 上下文塞到 ServletContext 中
* @param sce
*/
@Override
public void contextInitialized(ServletContextEvent sce) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
sce.getServletContext().setAttribute("app", applicationContext);
// 其他地方就可以通过 ServletContext#getAttribute 获取 spring 到上下文
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
然后需要在 web.xml 中配置监听器
<!-- 配置监听器 -->
<listener>
<listener-class>org.example.context.ContextLoaderListener</listener-class>
</listener>
然后再编写一个 Servlet
package org.example.web;
import org.example.tx.service.AccountService;
import org.springframework.context.ApplicationContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 拿到 ServletContext 域
ApplicationContext app = (ApplicationContext) this.getServletContext().getAttribute("app");
// 从域里获取容器初始化的时候塞进去的 spring 上下文
AccountService accountService = app.getBean(AccountService.class);
accountService.list().forEach(System.out::println);
}
}
再将这个 Servlet 配置到 web.xml 中
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>org.example.web.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/list</url-pattern>
</servlet-mapping>
启动 tomcat 访问 localhost:8080/list 看到控制台输出数据库里的 Account 表的数据就代表上下文创建成功。
上面我们写的 ContextLoaderListener 其实 spring 已经提供了,该监听器内部加载 spring 配置文件,并创建上下文对象,并储存到 ServletContext 域中,并且 spring 还提供了一个 WebApplicationContextUtils 供使用者获得应用上下文。
要使用 ContextLoaderListener 和 WebApplicationContextUtils 我们只需要做三件事情:
- 在 pom.xml 中添加 spring-web 的依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
- 在 web.xml 中配置 ContextLoaderListener 监听器
<!-- 初始化参数 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- 配置监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
web 项目整合 spring 完毕
spring mvc
开发步骤:
- maven 添加 spring mvc 相关坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
- 配置 spring mvc 核心控制器 DispatcherServlet
- 创建 Controller 类和视图页面
- 使用注解配置 Controller 类中业务方法映射地址
- 配置 Spring mvc 核心文件 spring-mvc.xml
<!-- 配置 spring mvc 前端控制器 -->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
使用方法网上有很多资源,这里不赘述,简单列一下执行流程
- 用户发送请求至前端控制器 DispatcherServlet
- DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器
- 处理器映射器找到具体的处理器(可以根据 xml 配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回 DispatcherServlet。
- DispatcherServlet 调用 HandlerAdapter 处理器适配器。
- HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器)
- Controller 执行完成返回 ModleAndView
- HandlerAdapter 将 Controller 执行结果 ModelAndView 返回给 DispatcherServlet
- DispatcherServlet 将 ModelAndView 传给视图解析器 ViewResolver
- ViewResolver 解析后返回具体的 view
- DispatcherServlet 根据 view 进行渲染视图(即将模型数据填充视图),最后响应给用户
spring mvc 常用注解
- @RestController = @RequestMapping + @ResponseBody(告诉处理器适配器直接回写数据,无需跳转逻辑视图)
- @PathVariable 从请求 url 中取参数
- 这部分内容很杂,待后期逐渐完善补充。。。
spring mvc 回写数据
由于 spring mvc 会将 Controller 返回的字符串拼接上前缀和后缀,然后通过这个视图名找到对应的视图资源返回给前端,所以我们必须得加上 @ResponseBody 注解告诉处理器适配器直接回写数据无需跳转视图。另外有种情况就是我们回写的可能是 json ,那么这个时候还需要配置一个 json 转换器,处理器适配器会使用 json 转换器将 Controller 返回的对象、集合转换成 json 字符串,下面贴出 xml 配置方式:
<!-- 配置 json 转换器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
</list>
</property>
</bean>
上面配置有点复杂,所以 spring mvc 也提供了一个简单的一键式配置方式,把上面的配置全部注释,用下面的注释替代就行了:
<!-- mvc 注解驱动 -->
<mvc:annotation-driven/>
@RequestBody
请求类型为 json 类型,并且 Controller 形参为集合类型的请求参数可以加上这个注解,spring mvc 框架会自动将参数注入进方法形参
mvc:resources 标签
<!-- 开放资源访问路径,不加这个标签,那诸如:localhost:8080/js/jquery.js 之类的请求会找对应的请求映射处理器,然后找对应的 Controller,没找到就返回 404 -->
<mvc:resources mapping="/js/**" location="/js/"/>
<mvc:resources mapping="/css/**" location="/css/"/>
<mvc:resources mapping="/font/**" location="/font/"/>
那如果觉得上面配置复杂,可以考虑下面的配置
<!-- 这个标签代表当 spring mvc 找不到对应资源时,交由底层容器(Tomcat、Weblogic等)去寻找资源并返回 -->
<mvc:default-servlet-handler/>
@RequestParam
// 别名、是否必输、默认值
@RequestParam(value = "nm", required = false, defaultValue = "defaultname") String name
@PathVariable
@GetMapping("/user/{id}")
public User getById(@PathVariable(name = "id") String id) {
}
另外值得提一下的是使用 @PathVariable 这个注解获取日期类型的参数时别用 yyyy/MM/dd 这种日期格式,很有可能会 404,因为 spring mvc 把 “/” 当成资源分隔符去找对应的资源映射了。
@RequestHeader
获取 HTTP 请求头信息
@CookieValue
cookie 是一种特殊的请求头,用这个注解可直接获取 cookie 里面的值,注意,请求头是键值对,cookie 也是键值对,cookie 是请求头更里面的一层键值对
文件上传
spring boot 的文件上传是依靠 spring mvc 文件上传实现的,而 spring mvc 的文件上传又是依靠 commons-fileupload 组件实现的
过滤器和拦截器
过滤器是在 doFilter 中完成前后处理的,并且 doFilter 方法没有返回值,而拦截器是通过更精细化的配置 preHandle、posthandle、afterCompletion 三个方法来控制一个请求的行为,方法是有返回值的,返回布尔值代表是否放行,一般用法如登录拦截器,拦截每个请求,若校验身份证通过则放行,否则使用 httpServletResponse 重定向,并返回 false 不放行。
spring boot 跨域请求配置
package com.kilobytech.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
/**
* 页面跨域访问Controller过滤
*
* @return
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
WebMvcConfigurer.super.addCorsMappings(registry);
registry.addMapping("/**")
.allowedHeaders("*")
.allowedMethods("POST","GET")
.allowedOrigins("*");
}
}
未完待续…
未完待续…