关于Spring的使用总结
- 简介
Spring框架是一种轻量级的、一站式的企业级开发解决方案
框架(framework):是一组设计思想、规范、API的精心组合,专门用来解决某一层次或领域的问题
轻量级(lightweight):此处是相对于EJB框架来说的,在资源占用、开发部署维护、学习成本等方面Spring都比EJB轻便
一站式(full-stack):即一步到位,Spring本身提供了丰富的功能特性,又直接整合了一批优秀框架,对于那些没有直接整合的其他框架,也提供了一层简单的封装让开发人员可以方便的手动整合,即Spring可以满足项目各个层次的要求。
Spring框架有众多模块,各个模块相互独立又能很好的合作
就我们现阶段的学习目标来说,上面这张图太过复杂,可简化如下:
整个Spring有三个核心模块:Spring容器、Spring AOP、声明式事务管理。这也是我们将要重点学习的内容
向maven项目添加Spring依赖:
<dependencies>
<!-- spring最基本的环境支持依赖,会传递依赖core、beans、expression、aop等模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.2.RELEASE</version>
</dependency> <!-- 提供了对其他第三方库的内置支持,如quartz等 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.3.2.RELEASE</version>
</dependency> <!-- spring处理对象关系映射的模块,传递依赖了jdbc、transaction等模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.3.2.RELEASE</version>
</dependency> <!-- spring对面向切面编程的支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.2.RELEASE</version>
</dependency> <!-- spring处理前端表现层的模块,即springMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.2.RELEASE</version>
</dependency> <!-- 第三方定时器框架 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.3</version>
</dependency> <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency> <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency> <!-- c3p0依赖 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5</version>
</dependency>
</dependencies> <build>
<plugins>
<!-- 指定JDK编译版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
- Spring容器
Spring容器是Spring的核心之一,用来存放、管理bean,也称为bean容器
bean:被Spring容器管理的Java对象称为bean,比如service类的对象、dao类的对象等。整个项目的运行是由各种bean支撑起来的,每个bean都是构成项目的一个组件
Spring容器对bean的管理包括:创建、注入依赖、初始化、销毁等。开发人员通过配置的方式指定Spring如何管理bean。
配置方式有两种:XML文件和注解,先使用XML文件的方式配置、学习Spring,最后再统一的讲解注解的配置方式
为什么要把对象交给Spring容器管理呢?
1 Spring必须把对象统一管理起来,才能实现其他功能特性,也就是说Spring提供的其他功能特性绝大部分都依赖Spring容器。
2 开发人员不用自己编码管理这些对象,可以专注于实现业务逻辑。
3 使对象的使用和创建相分离,方便修改和扩展
- Spring核心配置文件
Spring可在XML配置文件中定义bean、管理bean,配置文件的根元素是<beans>,文件名任意,目前命名为beans.xml,另外也经常使用applicationContext.xml等名称
Spring模块众多,可配置大量内容,Spring提供了多个schema文件分类描述这些内容,平时使用时,可以把常用的schema都引入配置文件中:
<?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:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
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/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd"> </beans>
- bean定义
可以使用<bean>标签配置、管理bean
public class UserService { }
<bean id="userService" class="com.rupeng.container.UserService"></bean>
id属性指定bean的id,使用时可以通过id找到该bean
name属性指定bean的名称,和id作用一样
class属性指定这个bean所表示的Java类的对象
一个<bean>标签就是一个bean定义,需要注意的是bean的本质是Java对象,而bean定义的本质是配置信息,但有些时候我们说bean,也可能表示bean定义,可根据具体语境进行区分,或者不用区分。这种使用class属性指定bean类型的配置方式,Spring在创建bean时会直接使用类的构造函数创建
- 容器对象:ApplicationContext
ApplicationContext是Spring的上下文对象,代表整个Spring框架,基本上也就代表了整个应用程序,它同时也是Spring容器对象,所有的bean都保存在这个对象中
@Test
public void test1() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("com/rupeng/container/beans.xml"); UserService userService = (UserService) applicationContext.getBean("userService");
UserService userService2 = applicationContext.getBean(UserService.class);
System.out.println(userService);
System.out.println(userService2);
}
ApplicationContext有多种创建方式,普通Java项目中可以使用ClassPathXmlApplicationContext创建,创建时需要指定beans.xml在类路径下的位置
Spring容器在创建的时候,会先读取所有的bean定义并保存起来,然后创建出bean,默认以单例的方式管理bean
getBean()用来在Spring容器中查找并返回bean,可以根据id或name查找,也可以根据类型查找。如果找不到会抛出异常,特别的,根据类型查找时如果找到多个也会抛出异常
- 其他创建bean的方式
即工厂方式和静态工厂方式,适合创建那些需要很多参数的对象
工厂方式:使用工厂对象的方法创建bean
工厂类:
public class UserServiceFactory { public UserService createUserService() {
return new UserService();
}
}
<bean id="userServiceFactory" class="com.rupeng.container.UserServiceFactory"></bean>
<bean id="userService" factory-bean="userServiceFactory" factory-method="createUserService"></bean>
静态工厂方式:使用工厂类的静态方法创建bean
工厂类:
public class UserServiceStaticFactory { public static UserService createUserService() {
return new UserService();
}
}
<bean id="userService" class="com.rupeng.container.UserServiceStaticFactory" factory-method="createUserService"></bean>
这三种创建bean的方式中,开发人员一般直接使用构造函数方式,Spring本身或者其他第三方框架一般使用工厂方式或静态工厂方式
- bean的作用域
在Spring中,可以指定bean的作用范围,默认是singleton(单例),即整个Spring容器运行期间该bean定义只会创建一个Java对象
<bean id="userService" class="com.rupeng.container.UserService" scope="singleton"></bean>
使用scope属性指定bean的作用域,可选值如下:
singleton:单例
prototype:原型,每次从容器中获取时都返回新的对象
request:对于每个HTTP请求都会创建一个新的对象
session:每个HTTP会话创建一个新的对象
globalSession:每个HTTP全局会话创建一个新的对象
application:每个ServletContext创建一个新的对象
虽然bean的作用域有多种,但只使用singleton和prototype已经满足绝大部分的需求,其他很少使用
有时候也把bean的作用域说成生命周期(含义很相似),另外生命周期也指bean从创建到销毁的过程(大家需要知道这个情况)
- 依赖注入
Spring可以把beanA所依赖的其他bean或者普通值(int、String)注入到beanA中,即依赖注入——DI(Dependency Inject)
基于setter方法的注入
UserDao类:
public class UserDao { }
UserService类:
public class UserService { private UserDao userDao; public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
<bean id="userDao" class="com.rupeng.container.UserDao"></bean>
<bean id="userService" class="com.rupeng.container.UserService">
<property name="userDao" ref="userDao"></property>
</bean>
使用<property>标签完成注入,可以使用ref属性指定依赖的bean的名称,也可以使用value属性注入普通值(int、String等),甚至还可以注入集合:
<!-- 注入普通值 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost/springDemo?characterEncoding=UTF8"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
</bean> <!-- 注入集合 -->
<bean id="team1" class="com.rupeng.container.Team">
<property name="members">
<list>
<value>蛋蛋</value>
<value>建国</value>
<value>王自健</value>
</list>
</property>
</bean>
基于构造函数的注入
public class User { private String name;
private Integer age; public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
<bean id="user1" class="com.rupeng.container.User">
<constructor-arg index="0" value="蛋蛋"></constructor-arg>
<constructor-arg name="age" value="16"></constructor-arg>
</bean>
<constructor-arg>标签用来给构造函数注入参数,可以使用index属性指定参数的索引位置,也可以使用name属性指定参数的名称
setter方式的注入比构造函数方式的注入更常用一些
- 控制反转(IoC)
在使用Spring之前,开发人员需要自己创建对象,并手动把依赖设置到对象中;现在,只需要通过配置,就可以把创建对象、注入依赖的操作交给Spring去完成,这种改变称为控制反转——IoC(Inversion of Control)。其实IoC和DI几乎是等价的,不用刻意区分
具有IoC功能的容器被称为IoC容器,所以Spring容器也是IoC容器。现在,ApplicationContext对象可以称为Spring上下文对象、Spring容器、bean容器、IoC容器等
控制反转把对象的创建和使用分离开来(解耦合),让开发人员可以更加专注于实现业务逻辑,另外还使得项目更易维护和扩展
- bean定义的其他配置项
abstract属性可以把bean定义成抽象的,用来被其他bean继承,本身并不会创建对象
parent属性用来指定要继承哪个bean定义
init-method属性用来指定创建bean对象后执行的初始化方法(值为方法名)
bean的总的创建过程为:
1 Spring扫描并保存配置文件中的bean定义
2(开始依次创建对象)创建一个对象之后,如果所有的依赖都已存在,则注入所有依赖,然后执行init-method(否则不注入也不执行init-method)
3(等所有的对象都创建出来之后)为还没有注入依赖的bean注入所有依赖,然后执行init-method
可以简化为:创建对象—> 注入依赖 —> init-method
destroy-method 用来指定销毁bean前执行的销毁方法,只对singleton作用域的bean有效(调用AbstractApplicationContext的destroy()可以测试出效果)
- AOP
代理
这里说的代理是指使用一个新类代替另一个类,并在另一个类原有功能的基础上进行增强处理。实现代理的方式有两种:JDK动态代理和CGLIB代理
目标类:原来的类,将要被新类代替的类
目标方法:目标类的方法,将要被增强的方法
代理类:提供功能增强的新类,将要代替目标类
- JDK动态代理
JDK动态代理是JDK内置的功能,基于接口在程序运行的时候动态的生成代理类。在JDK动态代理中目标类和代理类实现相同的接口,是兄弟关系:
具体实现步骤如下:
1 必须要有接口:
public interface MyInterface {
public void show();
}
如果目标类实现多个接口,在生成代理类时都会用到
2 目标类:
public class MyTarget implements MyInterface { @Override
public void show() {
System.out.println("目标类(被代理类)的show方法执行了");
}
}
3 用来对目标方法进行增强的InvocationHandler:
public class MyInvocationHandler implements InvocationHandler { private Object targetObject; public MyInvocationHandler(Object targetObject) {
this.targetObject = targetObject;
} @Override
public Object invoke(Object proxyObject, Method method, Object[] args) throws Throwable { System.out.println("可以在目标方法执行前进行预处理");
Object returnValue = method.invoke(targetObject, args);//执行目标方法
System.out.println("也可以在目标方法执行后进行后处理");
return returnValue;
}
}
传入目标对象的目的是可以执行目标方法
invoke方法的proxyObject参数表示代理类对象,method表示被调用的接口方法,args表示方法调用时传入的参数列表
method.invoke(targetObject,args) 表示执行目标方法
可以在目标方法执行前进行预处理
可以在目标方法执行后进行后处理
当使用代理类对象调用方法时,invoke方法就会被调用,从而实现增强处理
4 使用Proxy工具类生成代理类对象:
@Test
public void test1() { MyTarget targetObject = new MyTarget();
MyInvocationHandler invocationHandler = new MyInvocationHandler(targetObject); //生成代理类并直接返回代理类的对象
MyInterface proxyObject = (MyInterface) Proxy.newProxyInstance(MyTarget.class.getClassLoader(),
MyTarget.class.getInterfaces(), invocationHandler); proxyObject.show();
}
newProxyInstance方法可以直接返回生成的代理类的对象
生成代理类时需要目标类的类加载器、目标类实现的接口、增强处理实现
由于JDK代理是基于接口实现的,代理类只能对目标类的接口方法进行增强,不能对目标类独有方法进行增强;而且增强的方法都是public方法
由于接口继承了Object类,所以Object类的public方法也会被增强(除了getClass方法)
代理类对象、invocationHandler、目标对象之间的关系和调用过程:
- CGLIB代理
CGLIB(Code Generation Library)是高性能高质量的Java代码生成库,可以在程序运行时扩展类。CGLIB可以直接使用目标类生成代理类,不需要借助接口,所以目标类和代理类之间是父子关系
CGLIB生成代理类时很多地方和JDK动态代理相似,步骤如下:
1 目标类:
public class MyTarget { public void show() {
System.out.println("目标类(被代理类)的show方法执行了");
}
}
2 用来对目标方法进行增强的MethodInterceptor:
public class MyMethodInterceptor implements MethodInterceptor { private Object targetObject; public MyMethodInterceptor(Object targetObject) {
this.targetObject = targetObject;
}
@Override
public Object intercept(Object proxyObject, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("预处理");
Object returnValue = method.invoke(targetObject, args);
System.out.println("后处理");
return returnValue;
}
}
methodProxy包含目标类、代理类、被调用的方法等信息
3 使用Enhancer工具类生成代理类对象:
@Test
public void test1() { MyTarget targetObject = new MyTarget();
MyMethodInterceptor methodInterceptor = new MyMethodInterceptor(targetObject); Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyTarget.class);
enhancer.setCallback(methodInterceptor); MyTarget proxyObject = (MyTarget) enhancer.create();
proxyObject.show();
}
CGLIB可对除private以外的方法进行增强,另外注意不要使用final修饰类或者方法
和JDK动态代理相比,CGLIB生成代理类对象的过程慢,但代理类对象的性能高,非常适合生成单例类的代理对象。不过由于提倡使用接口编程,JDK动态代理也是非常常用的
- AOP的概念
AOP(Aspect-Oriented Programming),面向切面编程,是一种编程思想,是OOP的补充。
AOP是在OOP的基础上在编程的时候把公共的非业务逻辑操作抽取出来,然后使用代理的方式再把公共操作添加回去。AOP的好处是让公共操作和业务逻辑相分离(解耦合),既方便公共代码的编写和管理,也方便业务逻辑的实现,极大的简化了开发过程
OOP的编程方式:
ClassA a( ){ 记录日志 处理业务逻辑 } ClassB b( ){ 记录日志 处理业务逻辑 } ClassC c( ){ 记录日志 处理业务逻辑 } |
AOP的编程方式:
Aspect log( ){ 记录日志 } ClassA a( ){ 处理业务逻辑 } ClassB b( ){ 处理业务逻辑 } ClassC c( ){ 处理业务逻辑 } |
- AOP的一些概念
Aspect 切面,使用Java类表示,用来存放公共操作抽取成的方法
public class LogAspect { public void log() {
//记录日志
}
}
Advice 通知,其实就是公共操作抽取成的方法
通知有五种类型:
Before 在目标方法执行前执行
AfterReturning 在目标方法正常返回后执行
AfterThrowing 在目标方法抛出异常后执行
After 在目标方法执行后执行(无论是正常返回还是抛出异常)
Around 在目标方法执行前和执行后都可执行
Join point 连接点,代码中可被插入通知的点,比如方法调用、属性访问、异常处理等,但Spring AOP只支持方法调用连接点
Pointcut 切入点,用来指定哪些连接点将被插入通知(哪些方法是目标方法)
Target 目标对象(被代理对象)
Proxy 代理对象
Weaving 织入,把通知和目标方法连接起来的过程称为织入
- Spring AOP
Spring AOP是AOP的一种实现,基于代理(JDK动态代理和CGLIB代理),配合Spring容器,在运行时完成织入。
编程步骤
1 准备好目标类和目标方法
public class MyTarget { public void show() {
System.out.println("目标方法show()开始执行");
}
}
2 准备好切面类和通知
public class LogAspect {
//通知
public void log(JoinPoint joinPoint) {
System.out.println("切面方法log()开始执行");
}
}
通知类型需要通过配置指定
3 beans.xml:
<bean id="myTarget" class="com.rupeng.aop.MyTarget"></bean>
<bean id="logAspect" class="com.rupeng.aop.LogAspect"></bean>
<aop:config>
<aop:aspect ref="logAspect">
<aop:before method="log" pointcut="execution(* com.rupeng.aop.MyTarget.show(..))"/>
</aop:aspect>
</aop:config>
目标对象和切面对象都需要先配置成bean才有效果
- JoinPoint类
通知方法的第一个参数可声明为JoinPoint类型,通过该参数可访问目标对象、目标方法签名、目标方法参数等
Object targetObject = joinPoint.getTarget();
Signature signature = joinPoint.getSignature();
Object[] args = joinPoint.getArgs();//返回值是原方法参数的一份copy
特别的,当通知类型是Around时,可以使用子类ProceedingJoinPoint,方便控制是否执行目标方法,以及何时执行目标方法
- 切入点表达式
切入点使用切入点表达式指定哪些方法是目标方法。切入点表达式有多种类型,最常用的是execution,此外还有@target、@annotation等
execution用来描述目标方法的签名,由6个部分组成:
访问修饰符 返回值类型 类.方法名(参数列表) 异常声明
除了返回值类型、方法名、参数列表这三部分不可省略外,其余部分都可省略
各部分常用写法如下:
访问修饰符:一般省略,表示任意,但受代理方式的限制(CGLIB对private方法无效)
返回值类型:* 表示任意
类的包:包全名、前缀* 、..(某包和其子孙包)
类的类名: 类名、前缀*、*
方法名: 方法名、前缀*、*
参数列表: .. 表示任意
异常声明:一般省略,表示任意
示例:
execution(* com.rupeng.aop.MyTarget.show(..)) MyTarget的所有show方法
execution(* com.rupeng.aop.*.*(..)) aop包下所有类的所有方法
execution(* com.rupeng.aop..*.*(..)) aop包和其子孙包下所有类的所有方法
execution(* com.rupeng.aop.*.add*(..)) aop包下所有类的以add开头的方法
通知方法不会被自己或其他通知增强,同一个目标对象中的方法相互调用时,被调用的方法这次调用不会被增强(被其他对象的方法调用时会被增强)
@target描述了标注了某注解的类的所有方法
比如@target(com.rupeng.aop.MyAnnotation)表示标注了MyAnnotation注解的类的所有方法
@annotation描述了标注了某注解的方法
比如@annotation(com.rupeng.aop.MyAnnotation2)表示标注了MyAnnotation2注解的所有方法
特别的,切入点表达式还可使用 && || ! 进行逻辑运算。比如@target(com.rupeng.aop.MyAnnotation) && execution(* com.rupeng.aop.*.*(..)) 表示标注了MyAnnotation注解的类的所有方法和aop包下所有类的方法的交集
(注意&在xml中需要写成&)
- Spring AOP实现机制
1 在创建Spring容器时,Spring会读取并保存切面配置信息。创建bean后,如果这个bean匹配了切入点表达式,Spring就会创建其代理对象代理这个bean
2 使用代理bean第一次调用某个目标方法时,Spring会把所有匹配的通知和目标方法按照通知类型有序的组成一个调用链,并把此调用链缓存起来以便重复使用,然后执行此调用链
- 其他通知类型
after-returning通知
public class MyAspect { public void myAfterReturning(JoinPoint joinPoint, Object returnValue) {
System.out.println("myAfterReturning开始执行,目标方法的返回值为:" + returnValue);
}
}
此时通知方法可以定义第二个参数,表示目标方法的返回值
<aop:config>
<aop:aspect ref="myAspect">
<aop:after-returning method="myAfterReturning" pointcut="execution(* com.rupeng.aop.MyTarget.show(..))" returning="returnValue"/>
</aop:aspect>
</aop:config>
<aop:after-returning>标签的returning属性值需要和通知方法第二个参数的参数名一致。
after-returning类型的通知会在目标方法正常结束后执行,如果目标方法抛出异常则不执行
after-throwing通知
public class MyAspect { public void myAfterThrowing(JoinPoint joinPoint, Exception exception) {
System.out.println("myAfterThrowing开始执行");
}
}
切面方法的第二个参数可以是异常类型,表示目标方法抛出的异常,而且只有目标方法抛出此异常类或其子类时,才会执行切面方法
<aop:config>
<aop:aspect ref="myAspect">
<aop:after-throwing method="myAfterThrowing" pointcut="execution(* com.rupeng.aop.MyTarget.show(..))" throwing="exception"/>
</aop:aspect>
</aop:config>
<aop:after-throwing>标签的throwing属性值需要和切面方法的第二个参数的参数名一致
after通知
public class MyAspect { public void myAfter(JoinPoint joinPoint) {
System.out.println("myAfter开始执行");
}
}
通知方法会在目标方法结束后执行(无论目标方法是正常结束还是抛出异常),并且不可访问目标方法的返回值和抛出的异常
<aop:config>
<aop:aspect ref="myAspect">
<aop:after method="myAfter" pointcut="execution(* com.rupeng.aop.MyTarget.show(..))" />
</aop:aspect>
</aop:config>
around通知
public class MyAspect { public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around预处理");
Object returnValue = joinPoint.proceed();
System.out.println("around后处理");
return returnValue;
}
}
通知方法可以进行预处理和后处理,还可决定调用链是否继续执行
通知方法的返回值应该声明为Object类型,第一个参数应该声明为ProceedingJoinPoint,并且应该声明抛出Throwable异常类型
<aop:config>
<aop:aspect ref="myAspect">
<aop:around method="myAround" pointcut="execution(* com.rupeng.aop.MyTarget.show(..))" />
</aop:aspect>
</aop:config>
- AOP其他
AspectJ
除了Spring AOP,Spring还集成了第三方的AOP实现——AspectJ。AspectJ完整实现了AOP思想,有自己的编译器,在代码编译阶段完成织入。Spring AOP和Spring对AspectJ的集成相互独立,互不影响
强制让Spring AOP使用CGLIB生成代理
对于实现了接口的目标类,Spring AOP默认会使用JDK动态代理,这时目标类的非接口方法就不会被增强,可以通过配置强制让Spring AOP使用CGLIB生成代理
<aop:config proxy-target-class="true">
- Spring对数据库访问的支持
声明式事务管理
Spring内置了基于Spring AOP的声明式事务管理,以配置的方式统一管理整个项目的事务,极大的简化了开发人员的工作。声明式事务管理也是Spring的核心模块之一
配置步骤:
1 配置数据库连接池
2 根据操作数据库的方式选择对应的事务管理器
3 通过切入点表达式指定哪些方法需要织入事务,项目中一般为service包下的类的方法
4 指定事务属性,如 是否只读、传播行为、隔离级别等
<!-- 数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost/springDemo?characterEncoding=UTF8"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
</bean> <!-- 事务管理器 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean> <!-- 事务属性 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="*" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice> <!-- 织入事务 -->
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.rupeng.db..*.*(..))" />
</aop:config>
<aop:advisor>是旧版本Spring提供的一种织入方式,现在基本上只用来织入事务
- 事务传播行为
required:如果当前有事务,就使用当前的事务,如果没有,就开启新事务
supports:如果当前有事务,就使用当前事务,如果没有,则不开启事务
requires_new:如果当前有事务,则挂起当前事务,并开启新事务;否则直接开启新事务
not_supported:如果当前有事务,则挂起当前事务
nested:如果当前有事务,则开启一个嵌套事务;如果当前没有事务,开启新事务
mandatory:如果当前没有事务,则抛出异常
never:如果当前有事务,则抛出异常
- 事务隔离级别
事务四大特性有
原子性:事务下的操作要么全部成功,要么全部回滚
一致性:事务前后数据库都要处于一致状态(转账前后两个账户的总金额一致)
持久性:事务对数据的修改是持久的(即使数据库文件损坏也可通过日志等恢复)
隔离性:多事务并发访问时可能会造成数据安全问题,需要把事务隔离起来(加锁)
事务的隔离性会导致数据库并发访问性能降低,但也得保证数据的安全,所以数据库就提供了多种隔离级别以便针对不同的应用场景使性能和安全都得到兼顾
不同事务隔离级别的并发访问性能和数据安全性都不同,常见数据安全问题如下:
脏读:事务A读到了事务B未提交的数据(即使事务没有提交,SQL更新语句也会修改表)
不可重复读:事务A前后两次读取的同一行数据不同(期间事务B修改了这行数据并提交)
幻读:事务A删除表中全部数据后再次查询发现还有数据没删除(期间事务B插入新数据)
有四种数据库隔离级别:
read_uncommitted: 完全没有隔离,会出现脏读、不可重复读和幻读
read_committed: 简单隔离,不会出现脏读
repeatable_read: 中度隔离,不会出现脏读、不可重复读
serializable: 完全隔离,不会出现脏读、不可重复读和幻读
MySQL的默认隔离级别是repeatable_read、Oracle的默认隔离级别是read_committed,Spring在事务管理中默认使用当前数据库的隔离级别(即isolation="DEFAULT"),在实际使用时如果没有特殊要求,默认即可
- 事务回滚
默认情况下只有当事务方法抛出了RuntimeException或者Error时事务才会回滚(抛出检查异常不会回滚),但可以通过rollback-for属性指定需要回滚异常类型。也可以通过no-rollback-for属性指定不回滚的异常类型
- Spring对JDBC的简单封装
对于简单项目可以直接使用JDBC操作数据库。Spring提供了JDBCTemplate对JDBC操作进行了封装,方便开发人员使用
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean> <bean id="userDao" class="com.rupeng.db.UserDao">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
public class UserDao { private JdbcTemplate jdbcTemplate; public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public int insert(User user) {
String sql = "insert into T_Users(id,name,age) values(null,?,?)";
return jdbcTemplate.update(sql, user.getName(), user.getAge());
}
public User selectById(Long id) {
String sql = "select * from T_Users where id=?";
Object[] params = { id };
List<User> userList = jdbcTemplate.query(sql, params, new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setAge(rs.getInt("age"));
user.setName(rs.getString("name"));
return user;
}
});
if (userList.size() > 0) {
return userList.get(0);
} else {
return null;
}
}
}
- Resource资源接口
为了统一、方便的管理各种类型的资源,Spring提供了Resource资源接口以及众多和资源类型相关的实现类,部分实现类如下:
ClassPathResource
类路径下的资源,内部使用classLoader.getResourceAsStream(path)的方式获得资源的读取流
FileSystemResource
文件系统中的资源,内部使用new File(path)表示,并使用new FileInputStream(file)的方式获得资源的读取流
UrlResource
URL资源,内部使用new
URL(path)表示,并使用url.openConnection().getInputStream()的方式获得资源的读取流
创建ApplicationContext时配置文件路径的处理方式:
1 如果路径有classpath:前缀,则使用ClassPathResource表示
2 如果路径是URL,则使用URLResource表示
3 使用特定于ApplicationContext子类的方式表示,比如使用ClassPathXmlApplicationContext子类时,就使用ClassPathResource表示;使用FileSystemXmlApplicationContext子类时,就使用FileSystemResource表示
- 在配置信息中使用占位符
在XML或者注解配置中,可以使用 ${ propertyName } 的形式表示占位符,Spring容器创建时会从properties文件中获取对应的值替换占位符
properties文件使用<context:property-placeholder />标签加载,location属性指定文件位置,多个文件使用逗号分隔,system-properties-mode属性指定查找方式,取值如下:
NEVER 只从指定的文件中查找 FALLBACK 默认值,先查找指定文件,如果没有,再查找系统属性,即System.getProperties() OVERRIDE 先查找系统属性,如果没有,再查找指定文件 ENVIRONMENT 为兼容旧版本而保留的,和OVERRIDE一致,但不建议再使用 |
如果最终也没找到对应的属性值,则会抛出异常
示例:
jdbc.properties文件内容:
jdbc.driverClass=com.mysql.jdbc.Driver jdbc.jdbcUrl=jdbc:mysql://localhost/springDemo?characterEncoding=UTF8 jdbc.user=root jdbc.password=root |
<context:property-placeholder location="classpath:com/rupeng/placeholder/jdbc.properties" />
<!-- 数据库连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
<property name="user" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
- Spring表达式语言
Spring表达式语言,简称SpEL,语法和EL非常相似,但更加强大。SpEL虽然是Spring的一个模块,但可以脱离Spring容器独立使用,主要用来为Spring各个模块提供统一的表达式语言支持
除了表达式语言的一般功能外,SpEL还支持方法调用、创建集合对象、字符串模板等功能。在配置信息中使用时可简化配置,但实际使用频率很低,此处只简单了解
SpEL的格式是 #{ expression },要注意和配置信息中的${ }占位符区分开,它们是不同的技术
<bean id="user" class="com.rupeng.spel.User">
<property name="age" value="#{10+6}" />
<property name="phones" value="#{ {'18000000001','18000000002'} }" />
</bean>
- Spring任务调度
任务调度是指以固定频率、固定延迟或者固定时刻的方式调度执行指定的任务。
Spring内置了Spring Task并且集成了第三方任务调度框架Quartz,对任务调度提供了多种支持
Spring Task
Spring Task 是Spring内置的任务调度实现,用法简单方便
Spring Task 使用普通bean的方法作为任务方法,可以直接通过配置调度执行:
<task:scheduled-tasks>
<task:scheduled ref="myTask" method="doTask" fixed-rate="5000"/>
</task:scheduled-tasks>
method指定要调度执行的任务方法
fixed-rate上次任务开始后间隔指定时间执行下一次(固定频率)
fixed-delay上次任务完成后间隔指定时间执行下一次(固定延迟)
initial-delay 任务在创建好之后延迟多长时间开始第一次调度,默认立即开始调度
cron 使用cron表达式指定调度规则
Spring Task默认以单线程的方式调度执行所有任务,如果任务很多导致单个线程忙不过来,可以使用线程池:
<task:scheduler id="scheduler" pool-size="10"/>
<task:scheduled-tasks scheduler="scheduler">
<task:scheduled ref="myTask" method="doTask" fixed-rate="5000"/>
</task:scheduled-tasks>
注意:任务很多和一个任务很频繁不一样
Spring Task在固定频率调度下,如果一个任务很频繁就可能导致上次调度还没执行结束但下次调度又该开始了,这时Spring Task的处理方式是下次调度顺延到上次调度执行结束后再执行
一个<task:scheduled>标签在任务调度期间使用的始终都是同一个任务bean对象。
如果任务bean依赖其他bean,可以直接注入进来
- cron表达式
cron表达式用来指定调度规则,由6个或7个时间字段按顺序组成,并使用空格分隔:
秒 分 时 日期 月份 星期
秒 分 时 日期 月份 星期 年
注意:新版本的Spring Task只支持6个时间字段的形式,不支持年
名称 取值范围 支持的有特殊含义的字符
秒 0-59 , - / *
分 0-59 , - / *
时 0-23 , - / *
日期 1-31 , - / * ? L W
月份 1-12 , - / *
星期 1-7 , - / * ? L # (1表示星期日,2表示星期一)
特殊字符含义如下:
, 表示枚举 1,10,20(秒)表示第1秒,第10秒和第20秒
- 表示范围 2-6(星期)表示从星期一到星期五
/ 表示增量 0/5(秒)表示每隔5秒
* 表示每一个 *(时)表示每小时
? 为避免日期和星期冲突,在指定其中一个时,另一个使用?表示不指定
L 表示最后 6L(星期)表示某月的最后一个星期五
# 某月第几个星期几 7#2(星期)表示某月的第2个星期六
W 最近的有效工作日,9W(日期)表示如果9号在星期一到星期五之间,则就是9号;如果9号是星期六,则提前到8号(星期五);如果9号是星期日,则顺延到10号(星期一);注意不能跨月份
完整的例子:
0 0 9 * * ? 每天早上9点整
0/5 * * * * ? 每隔5秒钟
0 0 8 1 * ? 每月1号的8点整
0 0 8 ? * 2#1 每月的第一个星期一8点整
- Spring集成Quartz
Quartz是一个独立的任务调度框架,Spring简单封装了Quartz,方便在Spring中使用。
和Spring Task相比较而言,Quartz属于重量级的任务调度框架,使用方式也稍复杂些。
任务类需要继承QuartzJobBean:
public class MyJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException { }
}
配置JobDetailFactoryBean,用来生成JobDetail,JobDetail包含任务类和任务执行时依赖的数据
<bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="com.rupeng.quartz.MyJob"></property>
<property name="jobDataMap">
<map>
<entry key="username" value="蛋蛋"></entry>
</map>
</property>
</bean>
配置触发器trigger,指定任务调度规则:
<!-- 简单触发器 -->
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail" />
<property name="repeatInterval" value="2000" /><!-- 固定频率 -->
</bean> <!-- 也可以使用cron表达式指定调度规则 -->
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail" />
<property name="cronExpression" value="0/5 * * * * ?" />
</bean>
最后配置调度器
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="simpleTrigger" />
<ref bean="cronTrigger" />
</list>
</property>
</bean>
总结一下即:
1继承QuartzJobBean编写自己的任务类
2配置JobDetailFactoryBean,为任务类提供依赖的数据
3配置触发器指定调度规则
4配置调度器执行调度
任务每次被调度执行时都会创建新的任务类对象
Quartz的简单触发器按照固定频率的方式进行调度,和Spring Task的区别是Quartz固定频率调度时每次调度互不影响,即使上次调度还没执行完成,下次调度也会按时开始
大部分情况下项目中使用Spring Task或者Quartz都是可以的,可根据具体的应用场景灵活选择
- web项目中创建Spring容器
Spring提供了ContextLoaderListener这个servlet监听器,用来在web项目启动时创建Spring容器,并把创建好的Spring容器保存在ServletContext对象中
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"> <context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:beans.xml</param-value>
</context-param>
<!-- 初始化spring容器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener> <!-- 设置post请求编码和响应编码 -->
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<!-- 为true时也对响应进行编码 -->
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<!-- 设置为/*时才会拦截所有请求求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
servlet、JSP、JSLT等的maven依赖
<!-- servlet依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSP依赖 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!-- JSTL依赖 -->
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jstl-impl</artifactId>
<version>1.2</version>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
</exclusion>
</exclusions>
</dependency>
可以通过下面的方式在代码中获得Spring容器对象
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
- 注解方式配置Spring
使用注解把类标注为bean
Spring提供了@Component注解,相当于<bean>标签,可以把类标注为bean。同时可以指定bean的name。name默认为类名首字母小写形式,如UserService的bean name为userService
此外,还提供了@Controller、@Service、@Repository这三个注解,作用和@Component一样,但可读性更好,可以表明bean在项目的哪个层(Web层、service层、DAO层)
Spring的注解配置功能默认是关闭的,需要在beans.xml中使用<context:component-scan>标签开启。同时需要使用base-package属性指定到哪些包(及其子包)下去扫描注解,多个包之间可以使用逗号分隔
此外Spring还提供了
@Scope指定bean的作用域;
@PostConstruct指定bean的初始化方法(标注在初始化方法上)
@PreDestroy指定bean的销毁方法(标注在销毁方法上)
使用注解装配bean
Spring支持使用@Autowired、@Resource给bean注入依赖bean,也支持使用@Value给bean注入普通值
注入依赖bean时会基于两个方面:bean类型(以及泛型)和bean name
@Autowired优先基于类型注入
@Autowired可标注在字段、任意方法、构造函数上,优先根据bean的类型到Spring容器中查找并注入
如果找到多个类型匹配的bean(比如字段的类型是父类,有多个子类bean),会进一步根据泛型筛选
如果还是不能只筛选出一个bean,则再以字段名作为bean name进行筛选(也可以使用@Qualifier指定bean name)
如果最终也不能只筛选出一个bean,则抛出异常(找不到bean时也会抛出异常)
此外,@Autowired还可以注入ApplicationContext对象
@Resource优先基于bean name注入
Spring还支持JSR-250标准中的@Resource,@Resource可以标注在字段或者setter方法上,默认以字段名作为bean name(也可以手动指定bean name)到容器中查找,如果没找到,则再使用字段类型进行查找,也支持使用泛型进一步筛选
@Resource也可以注入ApplicationContext对象
对于@Autowired和@Resource,效果基本上是一样的,项目中一般主要使用其中一个,另一个作为辅助
@Value注入普通值
@Value可以注入普通值,比如String、int等,可标注在字段、setter方法上,也可标注在标注了@Autowired的构造函数或者方法的参数上面
在注入时,可以直接使用常量值,也可使用占位符或者Spring表达式
- 使用注解配置AOP
Spring直接使用了AspectJ框架定义的一些注解:@Aspect、@Pointcut、@Before、@AfterReturning、@AfterThrowing、@After、@Around等,但这些注解功能默认也是关闭的,需要使用<aop:aspectj-autoproxy/>标签开启
<context:component-scan base-package="com.rupeng.annotation" />
<aop:aspectj-autoproxy />
@Component
public class MyTarget { public String show(String message) {
System.out.println("目标方法show()执行了,message为:" + message);
return message;
}
}
@Component
@Aspect
public class MyAspect { @Pointcut("execution(* com.rupeng.annotation.aop.MyTarget.*(..))")
private void myPointcut1() {
} @Pointcut("execution(* com.rupeng.annotation.aop.*.*(..))")
private void myPointcut2() {
} @Before("myPointcut1()")
//@Before("execution(* com.rupeng.annotation.UserService.*(..))")
public void myBefore(JoinPoint joinPoint) {
System.out.println("myBefore");
} @AfterReturning(pointcut = "myPointcut1()", returning = "returnValue")
public void myAfterReturning(JoinPoint joinPoint, Object returnValue) {
System.out.println("myAfterReturning");
} @AfterThrowing(pointcut = "myPointcut1()", throwing = "exception")
public void myAfterThrowing(JoinPoint joinPoint, Exception exception) {
System.out.println("myAfterThrowing");
} @After("myPointcut1() && myPointcut2()")
public void myAfter(JoinPoint joinPoint) {
System.out.println("myAfter");
} @Around("myPointcut1()")
public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("myAround--before");
Object returnValue = joinPoint.proceed();
System.out.println("myAround--after");
return returnValue;
}
}
@Aspect相当于<aop:aspect>,用来定义切面
@Pointcut相当于<aop:pointcut>,用来定义可重用的切入点,标注在方法上,使用被标注的 方法名() 的方式引用
@Before相当于<aop:before>,用来定义Before通知,可直接指定切入点表达式,也可引用@Pointcut定义的可重用切入点表达式
- 使用注解配置Spring Task
可以很方便的使用@Scheduled注解配置Spring Task,标注在bean的方法上即可,但同样也需要使用<task:annotation-driven>标签开启Spring Task注解支持
@Scheduled(fixedRate=5000) 固定频率
@Scheduled(fixedDelay=5000) 固定延迟
@Scheduled(cron="*/5 * * * * ?") cron表达式
beans.xml开启task注解支持:
<task:annotation-driven scheduler="myScheduler"/>
<task:scheduler id="myScheduler" pool-size="10"/>
@Component
public class MyTask { @Scheduled(fixedDelay = 5000)
public void doTask() {
System.out.println(System.currentTimeMillis());
}
}
关于Spring的使用总结的更多相关文章
- 基于spring注解AOP的异常处理
一.前言 项目刚刚开发的时候,并没有做好充足的准备.开发到一定程度的时候才会想到还有一些问题没有解决.就比如今天我要说的一个问题:异常的处理.写程序的时候一般都会通过try...catch...fin ...
- 玩转spring boot——快速开始
开发环境: IED环境:Eclipse JDK版本:1.8 maven版本:3.3.9 一.创建一个spring boot的mcv web应用程序 打开Eclipse,新建Maven项目 选择quic ...
- Spring基于AOP的事务管理
Spring基于AOP的事务管理 事务 事务是一系列动作,这一系列动作综合在一起组成一个完整的工作单元,如果有任何一个动作执行失败,那么事务 ...
- [Spring]IoC容器之进击的注解
先啰嗦两句: 第一次在博客园使用markdown编辑,感觉渲染样式差强人意,还是github的样式比较顺眼. 概述 Spring2.5 引入了注解. 于是,一个问题产生了:使用注解方式注入 JavaB ...
- 学习AOP之透过Spring的Ioc理解Advisor
花了几天时间来学习Spring,突然明白一个问题,就是看书不能让人理解Spring,一方面要结合使用场景,另一方面要阅读源代码,这种方式理解起来事半功倍.那看书有什么用呢?主要还是扩展视野,毕竟书是别 ...
- 学习AOP之深入一点Spring Aop
上一篇<学习AOP之认识一下SpringAOP>中大体的了解了代理.动态代理及SpringAop的知识.因为写的篇幅长了点所以还是再写一篇吧.接下来开始深入一点Spring aop的一些实 ...
- 学习AOP之认识一下Spring AOP
心碎之事 要说知道AOP这个词倒是很久很久以前了,但是直到今天我也不敢说非常的理解它,其中的各种概念即抽象又太拗口. 在几次面试中都被问及AOP,但是真的没有答上来,或者都在面上,这给面试官的感觉就是 ...
- 为什么做java的web开发我们会使用struts2,springMVC和spring这样的框架?
今年我一直在思考web开发里的前后端分离的问题,到了现在也颇有点心得了,随着这个问题的深入,再加以现在公司很多web项目的控制层的技术框架由struts2迁移到springMVC,我突然有了一个新的疑 ...
- Spring之旅(2)
Spring简化Java的下一个理念:基于切面的声明式编程 3.应用切面 依赖注入的目的是让相互协作的组件保持松散耦合:而AOP编程允许你把遍布应用各处的功能分离出来形成可重用的组件. AOP面向切面 ...
- Spring之旅
Java使得以模块化构建复杂应用系统成为可能,它为Applet而来,但为组件化而留. Spring是一个开源的框架,最早由Rod Johnson创建.Spring是为了解决企业级应用开发的复杂性而创建 ...
随机推荐
- HDU 4901 多校4 经典计数DP
RT 最近不想写博客,累积了一周多的题目,今天趁着周日放假,全部补上吧 dp[i][j]表示前i个数,操作后的值为j的总个数 注意取或不取,有种完全背包的意味.因为数字小于1024,所以异或的结果也绝 ...
- tomcat和servlet容器的关系
- 第二周的java
- P 1021 个位数统计
转跳点:
- POJ 1320:Street Numbers
Street Numbers Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 2753 Accepted: 1530 De ...
- TP中统计指定字段的总数
如统计已激活设备数量和未激活设备数量 $condition = [ ['member_id', '=', $member_id] ]; $field = [ 'COUNT(IF(active_memb ...
- DevOps专题|Packer使用教程
什么是Packer 简单介绍一下自己 Packer 是一个轻量命令行工具, 能在几乎所有主流的操作系统上运行. 在给定一份配置文件的情况下, Packer 能为多种系统架构创建云主机镜像.同时 Pac ...
- Bandwagon 安装 Mysql 数据库
Bandwagon 安装 Mysql 数据库 1.搬瓦工系统准备 建议使用版本Centos6 x86_64,安装完成后,使用远程登陆软件登陆. 2.安装编译工具及库文件 yum -y install ...
- 布局基础<kotlin>2,自定义控件(整理自网络)
引导页 传送门 Android vector标签 PathData 画图 ViewPager 代码清单 activity_main.xml <?xml version="1.0&quo ...
- SQL inner join, join, left join, right join, full outer join
基本信息 创建两个表a1, a2. 两个表的重要差别是:a1 中有5,'wu',a2中没有. a2中有 4,'li',而a1中没有. 创建表和插入数据的代码如下: -- 创建a1表 create ta ...