为什么使用Spring?
spring是一个开源的轻量级的应用程序开发框架,其目的是简化企业的应用程序开发,降低侵入性,Spring提供的IOC和AOP功能,可以将组件之间的耦合度降到最低,便于后期的维护和升级,实现了软件的高内聚低耦合思想。
1)方便解耦,简化开发(高内聚、低耦合)
Spring就是一个大工厂,可以将所有对象创建和依赖关系维护,交给Spring管理
2)AOP编程的支持
Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等功能
声明式事务的支持
只需要通过配置就可以完成对事务的管理,而无需手动编程
3)方便集成各种优秀框架
Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如:Struts、Hibernate、MyBatis等)的直接支持
4)降低JavaEE API的使用难度
Spring 对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用等),都提供了封装,使这些API应用难度大大降低
IOC 控制反转
Ioc (Inversion of Control),中文叫做控制反转,这是一个概念,也是一种思想。控制反转,实际上就是值对一个对象的的控制权的反转。
例子:
比如说 现在有一个Employee类
:
public class Employee implements Serializable {
private static final long serialVersionUID = 7833677928421758477L;
private Integer id;
private String name;
private Double salary;
private Integer age;
private Status status;
// 省略get/set
}
现在需要使用这个对象,我们正常的方式是这样用的:
@Test
void Test() {
Employee employee = new Employee();
employee.setName("Lang");
employee.setAge(18);
employee.setSalary(9999.99);
}
在这种情况下,Employee 对象的控制权在Test()这个方法里面,这样的话,Employee和 Test() 高度耦合。如果在其他的地方需要使用到这个Employee对象,那就得重新创建,也就是说,在这个例子中,对象的创建,初始化,销毁等操作,都是由开发者来完成,如果能够将这些操作交给容器来管理,开发者就可以极大的从对象的创建中解脱出来。
使用Spring 之后,我们可以将对象的创建、初始化、销毁等操作交给Spring 容器来管理。就是说,在项目启动时,所有的Bean 都将自己注册到Spring 容器中去(如果有必要的话),然后如果其他Bean 需要使用到这个Bean ,则不需要自己去 new,而是直接去Spring 容器去要。通过一个简单的例子看下这个过程。
首先创建一个普通的Maven 项目,然后引入spring-context 依赖,如下:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
</dependencies>
接下来 在resources文件下创建一个 Spring的配置文件。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="com.java.Employee" id="Employee" />
</beans>
class 属性表示需要注册的bean 的全路径,id 则表示bean 的唯一标记。
接下来 加载这个配置文件:
public static void main(String[] args) {
ClassPathXmlApplicationContext con = new ClassPathXmlApplicationContext("applicationContext.xml");
}
执行Main 方法时,配置文件就会被加载,进而在Spring中初始化 一个 Employee 实例。
Bean 实例的获取
public static void main(String[] args) {
ClassPathXmlApplicationContext con = new ClassPathXmlApplicationContext("applicationContext.xml");
// Employee employee = (Employee) con.getBean("Employee");
Employee employee = con.getBean("Employee",Employee.class);
System.out.println(employee);
}
输出结果:
“init 是在Employee 构造方法中输出的”
属性注入(DI):
a) 构造方法注入:
通过bean 的构造方法 给bean 的属性注入值。
- 1.给bean 添加对应的构造方法
- 2.在xml中注入bean 这里需要注意的是,它会根据注入的参数类型/个数 去匹配对应的构造方法。
b)set方法注入:
如上类似,它是通过bean 的set方法注入属性
c)外部bean注入:
有时候,我们需要使用一些外部bean,这些bean 可能没有构造方法 ,所以无法通过new来获取,而是通过builder / 特定的静态方法 来构造。
例如:
这里 Calendar 这个实例,我们可以使用 静态工厂注入 或者 实例工厂注入
静态工厂:
public class CalendarUtils {
private static Calendar calendar;
public static Calendar getInstance() {
if (calendar == null) {
calendar = Calendar.getInstance();
}
return calendar;
}
}
。。。。。。。。。。。。后面都是简单注入,懒得写了,
Java 配置:
@configuration 注解表示这是一个Java配置类,配置类的作用类似于 applicationContext.xml
@Configuration
public class Config {
@Bean("employeeInstance") // 不指定则默认为方法名
Employee employee() {
return new Employee();
}
}
这样就把一个bean注册到spring 中了
使用:
AnnotationConfigApplicationContext con = new AnnotationConfigApplicationContext(Config.class);
Employee employee = con.getBean("employeeInstance", Employee.class);
System.out.println(employee);
自动化配置:
例如现在有一个 UserService ,我希望在自动化扫描时,这个类能够自动注册到Spring容器中去,那么可以给该类添加一个@Service
,作为一个标记。
与@Service
注解功能类似的注解 一共有四个:
@Component
:在其他组件上使用
@Repository
: 在Dao层使用
@Service
:在 Service 层使用
@Controller
:在Controller 层使用
这四个中,另外三个都是基于 @Component
做出来的,而且从目前的源码来看,功能基本一直,那么为什么要搞三个?,主要是为了在不同的类上添加时方便
例子:
使用Java配置包扫描:
使用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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.java.service" />
</beans>
ClassPathXmlApplicationContext con = new ClassPathXmlApplicationContext("applicationContext.xml");
EmployeeService bean = con.getBean("employeeService", EmployeeService.class);
bean.getAll();
对象注入
当使用自动扫描时的对象注入有三种方式:
@Autowired
是根据类型去查找,然后赋值,这个类型只可以有一个对象。否则就会报错。 它也可以配合@Qualifier
实现通过变量名查找到变量。
@Resoutce
是根据名称去查找,默认情况下,定义的变量名,就是查找的名称。
@Injected
@Repository
public class UserDao {
public String getName() {
return "Test";
}
}
注册后 ,使用:
@Service
public class EmployeeService {
@Autowired
UserDao userDao;
public List<String> getAll() {
String name = userDao.getName();
return Arrays.asList(name);
}
}
条件注解
条件注解 就是在满足某一个条件的情况下,才生效的注解
案例: 假如 需要在Windows 中 查看文件夹的目录命令是 dir,而在Linx 中查看文件夹目录的命令是ls,我现在希望当系统运行Windows 上时,自动打印出windows 上的目录命令,Linux运行时,则自动展示 linux上的目录命令。
首先 定义一个命令显示接口:
public interface ShowCmd {
String showCmd();
}
接着 win / Linux 分别实现该接口
public class WinShowCmd implements ShowCmd {
public String showCmd() {
return "dir";
}
}
public class LinuxShowCmd implements ShowCmd {
public String showCmd() {
return "ls";
}
}
接下来 定义两个条件。一个时windows 下的条件,一个是Linux 下的条件。
public class WindowsCondition implements Condition {
public boolean matches(ConditionContext con, AnnotatedTypeMetadata anno) {
return con.getEnvironment().getProperty("os.name").toLowerCase().contains("win"); // 获取当前允许环境的系统名称 如果包含"win"
// 返回true
}
}
public class LinuxCondition implements Condition {
public boolean matches(ConditionContext con, AnnotatedTypeMetadata anno) {
return con.getEnvironment().getProperty("os.name").toLowerCase().contains("linux"); // 获取当前允许环境的系统名称 如果包含"linux"
// 返回true
}
}
接下来 在注册bean 的时候就可以去配置条件注解了。
自动配置类:
@Bean("cmd")
@Conditional(WindowsCondition.class) // 条件判断注解 根据此条件类去觉得是否注册bean
ShowCmd winCmd() {
return new WinShowCmd();
}
@Conditional(LinuxCondition.class) // 条件判断注解 根据此条件类去觉得是否注册bean
@Bean("cmd")
ShowCmd linuxCmd() {
return new LinuxShowCmd();
}
这里 一定要给两个bean 取相同的名字,这样在调用时,才可以自动匹配,因为最终只会注册一个bean ,当条件中的 matches 方法返回true 的时候,这个bean的定义才会生效
AnnotationConfigApplicationContext con = new AnnotationConfigApplicationContext(Config.class);
ShowCmd showCmd = con.getBean("cmd", ShowCmd.class);
System.out.println(showCmd.showCmd());
Bean 的作用域
AnnotationConfigApplicationContext con = new AnnotationConfigApplicationContext(Config.class);
ShowCmd showCmd1 = con.getBean("cmd", ShowCmd.class);
ShowCmd showCmd2 = con.getBean("cmd", ShowCmd.class);
System.out.println(showCmd1 == showCmd2); // true
如上,从Spring 容器中,多次获取同一个Bean ,默认情况下,获取到的实际上是同一个实例,我们也可以自己手动配置:
xml 配置
<bean id="employee" class="com.java.Employee" scope="prototype" />
通过在xml 节点中,设置scope 属性,我们可以调整默认的实例个数。
scope的值默认为 singleton(默认)
,表示这个Bean在spring容器中,是以单例的模式存在的
如果scope 的值为 prototype
,表示这个Bean 在spring 容器中不是单例,多次获取将会获取到不同的实例。
除了 singleton 和 prototype 之外,还有两个取值,request
,session
,这两个取值在 web 环境下有效。。
除了可以在xml 中配置,也可以在Java 中配置
@Bean("employeeInstance") // 不指定则默认为方法名
@Scope("prototy")
Employee employee() {
return new Employee();
}
AOP
Aop,面向切面编程,这是对面向对象思想的一种补充。通俗来说,就是在不改变程序源码的情况下,动态的增强方法的功能,常见的使用场景:日志,性能监控,事务
在Aop中有几个常见的概念:
- 切点:要增加代码的地方,称作切点。
- 通知:通知就是向切点动态添加的具体代码。
- 切面:切点 + 通知 就是一个切面
- 连接点:切点所在的方法。
实现:
CGLIB:
JDK:
MyUser 接口
public interface MyUser {
void selectUser();
}
MyUserImpl 实现类
public class MyUserImpl implements MyUser {
public void selectUser() {
System.out.println("My name is User");
}
}
UserProxy 代理类
public class UserProxy implements InvocationHandler {
private Object obj;
public UserProxy(Object obj) {
this.obj = obj;
}
public Object getUserProxy() {
ClassLoader classLoader = obj.getClass().getClassLoader();
Class<?>[] interfaces = obj.getClass().getInterfaces();
return Proxy.newProxyInstance(classLoader, interfaces, this);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
Object invoke = method.invoke(obj, args);
System.out.println("after");
return invoke;
}
}
测试:
public class Main {
public static void main(String[] args) {
MyUser myUser = new MyUserImpl();
UserProxy userProxy = new UserProxy(myUser);
MyUser myUserSrervice = (MyUser) userProxy.getUserProxy();
myUserSrervice.selectUser();
}
}
切点定义:注解 + 规则定义
使用注解进行切面
先自定义一个注解:
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
}
Java 自动配置类
@ComponentScan // 开启包扫描 把当前包下的带注解的bena注册到spring
@Configuration // 代表是一个配置类
@EnableAspectJAutoProxy // 开启自动代理
public class Config {
}
定义一个接口
public interface MathCalculator {
int add(int a, int b);
int sub(int a, int b);
}
其实现类
@Component
public class MathCalculatorImpl implements MathCalculator {
@Action
@Override
public int add(int a, int b) {
System.out.println("add 方法执行了");
return a + b;
}
@Override
public int sub(int a, int b) {
System.out.println("sub 方法执行了");
return a - b;
}
}
切面类:
@Aspect // 表示这个一个切面
@Component
public class LogAspect {
// 前置通知
@Before("@annotation(Action)")
public void before(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
System.out.println("Before Methods " + name);
}
}
运行测试:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
MathCalculator matchCalculator = ctx.getBean(MathCalculator.class);
matchCalculator.add(1, 3);
}
五大通知 类型
@Aspect // 表示这个一个切面
@Component
public class LogAspect {
/**
* 前置通知 在方法执行之前执行
*
* @param joinPoint
*/
@Before("@annotation(BeforeAction)")
public void before(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法执行开始了");
}
/**
* 后置通知 在方法执行之后执行
*
* @param joinPoint
*/
@After("@annotation(AfterAction)")
public void after(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法执行结束了");
}
/**
* 返回通知 r 为返回的结果,如果被拦截的方法没有返回值 ,如果返回的参数类型不匹配也不会执行。
* 则此方法不会执行
*
* @param joinPoint
* @param r
*/
@AfterReturning(value = "@annotation(AfterReturningAction)", returning = "r") // 这个 r 要于下面参数的一致,这是拦截那个方法的返回值
public void returning(JoinPoint joinPoint, Integer r) {
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法的的返回值是" + r);
}
/**
* 异常通知 ,当目标方法抛出异常时,该方法触发
*
* @param joinPoint
* @param e 异常的参数
*/
@AfterThrowing(value = "@annotation(AfterReturningAction)", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法出现异常,异常通知:" + e.getMessage());
}
/**
* 环绕通知
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("@annotation(MyAround)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 这里类似于 反射的method.invoke // 我们可以在这个方法的前后分别添加日志,相等于前置/后置 通知
Object proceed = joinPoint.proceed();
return 4;
}
}
统一定义 切入点:
@Pointcut("@annotation(BeforeAction)") // 统一定义切入点
public void ponitcut() {
}
/**
* 前置通知 在方法执行之前执行
*
* @param joinPoint
*/
@Before("ponitcut()")
public void before(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法执行开始了");
}
通知执行顺序
正常情况下: Around => Before => Methods => After => AfterReturning
如果方法中抛出了异常: Around => Before => Methods => After => AfterThrowing
使用注解 切入其实是侵入式的,我们使用的更多的是 自定义拦截规则
例:
@Pointcut("execution(* com.java.service..*Impl.*(..))") // 统一定义切入点
// 返回值类型 包名.类名(参数类型)
public void ponitcut() {
}
Aop 事务
…