Java代码审计入门基础之Spring
目前大部分的 Java 互联网项目,都是用 SSM(Spring MVC + Spring + MyBatis)框架组合搭建的,当然现在SpringBoot也很流行,但是学习Java代码审计,先从SSM框架开始学起,这篇文章将会先介绍Spring的基础。
三层架构
终于来到Spring框架了,前面讲了Spring MVC和MyBatis框架,现在就讲了Spring框架的作用是什么以及如何在开发中去配置。
Spring框架是Java应用最广的框架,是一个轻量级的开源框架,它的主要理念包括 IoC (Inversion of Control,控制反转) 和 AOP(Aspect Oriented Programming,面向切面编程)。Spring框架的结构,见下图。
- Data Access/Integration(持久层):包含有JDBC、ORM、OXM、JMS和Transaction模块。
- Web层:包含了Web、Web-Servlet、WebSocket、Web-Porlet模块。
- AOP模块:提供了一个符合AOP联盟标准的面向切面编程的实现。
- Core Container(IoC):包含有Beans、Core、Context和SpEL模块。
- Test模块:支持使用JUnit和TestNG对Spring组件进行测试。
Spring框架能实现的功能:
①Spring 能帮我们根据配置文件创建及组装对象之间的依赖关系。
②Spring 面向切面编程能帮助我们无耦合的实现日志记录,性能统计,安全控制。
③Spring 能非常简单的帮我们管理数据库事务。
④Spring 还提供了与第三方数据访问框架(如Hibernate、JPA)无缝集成,而且自己也提供了一套JDBC访问模板来方便数据库访问。
⑤Spring 还提供与第三方Web(如Struts1/2、JSF)框架无缝集成,而且自己也提供了一套Spring MVC框架,来方便web层搭建。
⑥Spring 能方便的与Java EE(如Java Mail、任务调度)整合,与更多技术整合(比如缓存框架)。
Spring框架的意义:
低侵入,方便解耦,简化开发
通过 Spring 提供的 IoC 容器,可以将对象间的依赖关系交由 Spring 进行控制,避免硬编码所造成的过度程序耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。
AOP 编程的支持
通过 Spring 的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP 实现的功能可以
通过 AOP 轻松应付。
声明式事务的支持(基于切面和惯例)
可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理,
提高开发效率和质量。
方便程序的测试
可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可
做的事情。
方便集成各种优秀框架
Spring 可以降低各种框架的使用难度,提供了对各种优秀框架( Struts、 Hibernate、 Hessian、Quartz
等)的直接支持。
降低 JavaEE API 的使用难度
Spring对JavaEE API(如 JDBC、 JavaMail、远程调用等)进行了薄薄的封装层,使这些 API 的
使用难度大为降低。
IoC(控制反转)
IoC并不是什么技术,而是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。
正控:若要使用某个对象,需要自己去负责对象的创建,即采用new的方式。
反控:若要使用某个对象,只需要从 Spring 容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了Spring框架。
IoC的作用:削减计算机程序中的耦合(即解除我们代码中的依赖关系)。
耦合:程序间的依赖关系,包括类之间的依赖和方法之间的依赖。
解耦:降低程序间的依赖关系。
在实际开发中,应该做到:编译期间不依赖,运行时才依赖。
解耦的思路:
1、使用反射来创建对象,而避免使用new关键字来创建对象;
2、通过读取配置文件来获取要创建的对象的全限定类名。
接下来,我将通过自定义一个BeanFactory类(先不使用Spring框架),使用工厂模式的方式去解耦程序,从而讲解IoC的大概原理。但首先,我会先通过传统的方式(使用工厂模式前)写一个demo,然后再通过工厂模式去优化代码。
传统的创建对象方式
先新建一个项目,目录结构如下:
在该项目中,我将模拟表现层,通过ui中的类Client来调用业务层service中的接口来模拟保存账户(这里只是模拟保存账户,在console中打印“保存了账户”该信息,并不是真的创建并保存一个账户)。
表现层中的com.nick.ui.Client.java,代码如下:
package com.nick.ui;
import com.service.IAccountService;
import com.nick.service.impl.AccountServiceImpl;
/**
* 模拟一个表现层,用于调用业务层
*
*/
public class Client {
public static void main(String[] args) {
IAccountService as = new AccountServiceImpl();
System.out.println(as);
as.saveAccount();
}
}
业务层中的com.service.IAccountService.java,代码如下:
package com.nick.service;
public interface IAccountService {
void saveAccount();
}
业务层中的实现类com.nick.service.impl.AccountServiceImpl,代码如下:
package com.nick.service.impl;
import com.nick.dao.IAccountDao;
import com.nick.dao.impl.AccountDaoImpl;
import com.nick.service.IAccountService;
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao = new AccountDaoImpl();
public void saveAccount(){
accountDao.saveAccount();
}
}
在业务层中通过调用持久层接口的方法来模拟保存账户。
持久层中的com.nick.dao.IAccountDao.java,代码如下:
package com.nick.dao;
public interface IAccountDao {
void saveAccount();
}
持久层中的实现类com.nick.dao.impl.AccountDaoImpl.java,代码如下:
package com.nick.dao.impl;
import com.nick.dao.IAccountDao;
public class AccountDaoImpl implements IAccountDao{
public void saveAccount(){
System.out.println("已经保存了账户");
}
}
然后在Client类右键点击运行:
运行成功,这里成功“保存了账户”,并打印了业务层对象。
但是,在上面的代码中,业务层调用持久层,并且此时业务层在依赖持久层的接口和实现类。如果此时没有持久层实现类,编译将不能通过。 在表现中也依赖了业务层的接口和实现类,见下图(通过new的方式来产生依赖),这种编译期依赖关系,应该在我们开发中杜绝,因此我们需要优化代码来解决。
使用工厂模式来解决程序耦合
下面,我将对上述代码进行优化,通过工厂模式来减少程序间的耦合。
优化后的项目目录结构如下:
首先创建一个BeanFactory类,一个创建bean的工厂,用于创建service和dao对象。
方法:
1、需要一个配置文件来配置service和dao
配置内容为:key=value -> 唯一标志名=全限定类名
2、通过读取配置文件中的配置内容,反射创建对象
com.nick.factory.BeanFactory.java,代码如下:
package com.nick.factory;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
*一个创建bean的工厂
* 用于创建service和dao对象
*方法:
* 1、需要一个配置文件来配置service和dao
* 配置内容为:key=value :唯一标志=全限定类名
* 2、通过读取配置文件中的配置内容,反射创建对象
*/
public class BeanFactory {
//定义一个Properties对象
private static Properties props;
//定义一个Map,用于存放我们的对象,该Map对象称之为“容器”
private static Map<String,Object> beans;
//使用静态代码块为Properties对象赋值
static {
try {
//实例化对象
props = new Properties();
//获取Properties文件的流对象
InputStream in = BeanFactory.class.getClassLoader()
.getResourceAsStream("bean.properties");
props.load(in);
//产生单例对象的方式
//实例化容器
beans = new HashMap<String,Object>();
//取出配置文件中的所有的key
Enumeration keys = props.keys();
//遍历枚举中的值
while(keys.hasMoreElements()){
//取出每个key
String key = keys.nextElement().toString();
//根据key获取bean的路径
String beanPath = props.getProperty(key);
//反射创建对象
Object value = Class.forName(beanPath).newInstance();
//把key和value存入容器中
beans.put(key,value);
}
}catch (Exception e){
throw new ExceptionInInitializerError("初始化Properties对象失败");
}
}
/**
*根据bean的名称获取bean对象
* @param beanName
* @return
*/
//此时获取的对象是单例的
public static Object getBean(String beanName){
//直接通过传入bean的名字来获取容器中的bean对象
return beans.get(beanName);
}
}
在resources目录下新建一个bean的配置文件bean.properties:
accountService=com.nick.service.impl.AccountServiceImpl
accountDao=com.nick.dao.impl.AccountDaoImpl
接下来,把原来代码中使用new方式引入依赖的两行代码注释掉,改成使用工厂模式的方法,直接去获取容器中所需的对象。
com.nick.service.impl.AccountServiceImpl,代码如下:
package com.nick.service.impl;
import com.nick.dao.IAccountDao;
import com.nick.factory.BeanFactory;
import com.nick.service.IAccountService;
public class AccountServiceImpl implements IAccountService {
//private IAccountDao accountDao = new AccountDaoImpl();
IAccountDao accountDao = (IAccountDao) BeanFactory.getBean("accountDao");
public void saveAccount(){
accountDao.saveAccount();
}
}
com.nick.ui.Client.java,代码如下:
package com.nick.ui;
import com.nick.factory.BeanFactory;
import com.nick.service.IAccountService;
/**
* 模拟一个表现层,用于调用业务层
*
*/
public class Client {
public static void main(String[] args) {
//测试循环创建的对象是单例还是多例
for(int i=0;i<5;i++) {
//IAccountService as = new AccountServiceImpl();
IAccountService as = (IAccountService) BeanFactory.getBean("accountService");
System.out.println(as);
as.saveAccount();
}
}
}
注:上述代码中的for循环只是为了证明程序中创建的对象是单例而不是多例的,这里可把for循环去掉。
运行结果如下:
看到这里,大家可能会有疑问,明明优化前的代码看起来更简洁更简单,反而使用工厂模式后代码量多了,获取对象的方式看起来也更复杂,感觉好像很没必要,这里大家不需要想太多,只需要记住,在实际开发中,我们力求低耦合,程序之间的耦合不可能完全消除的,使用Ioc的方式是为了减少程序之间的耦合,降低对象之间的依赖程度。
使用 spring 的 IoC 解决程序耦合
后面,我将会介绍获取IoC容器中bean的两种方式,分别是通过xml文件配置和通过注解的方式。在实际开发中,一般使用注解的方式比较多,下面我就只讲基于注解的IoC配置方式。
基于注解的IoC配置方式
首先,重新创建一个新的Maven项目,然后配置pom.xml中的依赖,Spring依赖的配置如下,使用注解需要用到的jar包是aop包,如下图所示。
注意:在配置pom.xml文件的时候,idea的右下角可能会弹出下图的提示,此时只需点击红色箭头指向的地方即可自动引入maven中配置的依赖jar包。
该项目使用的例子与上面的项目的例子相似,这里只是使用了Spring IoC中的注解来获取bean对象而已。新项目目录结构如下图:
下面开始介绍各个注解:
首先,先上项目的代码:
这里表现层的com.nick.ui.Client.java的代码如下:
package com.nick.ui;
import com.nick.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 模拟一个表现层,用于调用业务层
*
*/
public class Client {
public static void main(String[] args) {
//获取容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//
IAccountService as = (IAccountService) ac.getBean("accountServiceImpl");
System.out.println("对象为:"+as);
//先不使用对象中的方法
//as.saveAccount();
}
}
持久层的com.nick.dao.IAccountDao.java代码如下:
package com.nick.dao;
public interface IAccountDao {
void saveAccount();
}
持久层的实现类com.nick.dao.AccountDaoImpl.java代码如下:
package com.nick.dao.impl;
import com.nick.dao.IAccountDao;
import org.springframework.stereotype.Component;
@Component
public class AccountDaoImpl implements IAccountDao{
public void saveAccount(){
System.out.println("已经保存了账户");
}
}
业务层的com.nick.service.IAccountService.java代码如下:
package com.nick.service;
public interface IAccountService {
void saveAccount();
}
业务层的实现类com.nick.service.AccountServiceImpl.java代码如下:
package com.nick.service.impl;
import com.nick.dao.IAccountDao;
import com.nick.service.IAccountService;
import org.springframework.stereotype.Component;
@Component
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;
public AccountServiceImpl(){
System.out.println("成功创建了对象");
}
public void saveAccount(){
accountDao.saveAccount();
}
}
其中,Spring的主配置文件bean.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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!--告知spring在创建容器时要扫描的包,配置所需要的标签不是在beans的约束中,而是一个名称为
context名称空间和约束中-->
<context:component-scan base-package="com.nick"></context:component-scan>
</beans>
注:该配置文件的名字不是固定的,但有时候很多人习惯使用applicationContext.xml这个名字。
用于创建对象的注解
它们的作用就和在XML配置文件中编写一个<bean>标签实现的功能是一样的。
例如,使用@Repository注解:
修改代码,执行结果如下:
例如,在com.nick.service.AccountServiceImpl.java改为使用@Service注解:
以上三个注解他们的作用和属性与@Component是一模一样的,它们三个是spring框架为我们提供明确的三层使用的注解,使我们的三层对象更加清晰,所以我们可以代码中的@component注解改成对应层的注解@Controller、@Service和@Repository。
另外需要注意的是,使用@Component注解后,必须在bean.xml配置文件中添加以下配置:
<context:component-scan base-package="com.nick"></context:component-scan>
而且,在基于注解的IoC配置方式中的配置文件,其中的约束跟基于xml的IoC配置方式使用的约束是不一样的:
基于xml的IoC配置
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200724093940155.png?x-oss-proc![在这里插入图片描述](https://img-blog.csdnimg.cn/20200724094017397.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNTQ2NTk2,size_16,color_FFFFFF,t_70)
ess=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNTQ2NTk2,size_16,color_FFFFFF,t_70)
</beans>
基于注解的IoC配置
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
</beans>
上述代码运行结果如下:
在上述代码中,我把Client.java中调用业务层对象的方法那一行的代码注释掉了,下面我把该注释去掉,看看结果会是怎么样:
代码运行过程中,报错信息指向了业务层的实现类com.nick.service.AccountServiceImpl.java的18行,这里报错说是空指针异常:
由上图代码可见,我们在第12行只定义了对象accountDao,但是这里并没有对该对象初始化,没有为它赋值。这时候我们需要用到一个新的注解@Autowired。
使用@Autowired注解后的com.nick.service.AccountServiceImpl.java代码如下:
然后重新运行,这时候成功调用了业务层对象的方法,结果如下:
用于注入数据的注解
他们的作用就和在xml配置文件中的bean标签中写一个<property>标签的作用是一样的。
作用:
自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。
如果ioc容器中没有任何bean的类型和要注入的变量类型匹配,则报错。
如果Ioc容器中有多个类型匹配时:见下面的讲解。
出现位置:
可以是变量上,也可以是方法上
若容器中有两个及其以上的bean跟要注入的对象类型一致的情况
为了说明,我在com.nick.dao.impl目录下新建一个AccountDaoImpl2.java文件:
同时,也修改原来的业务层的实现类中代码:
修改Client.java文件,然后运行结果如下:
从上图的报错信息可知,在使用了注解@Autowired进行自动注入的时候,由于同时存在两个类型均为IAccountDao的bean对象,所以不知道注入容器中的哪个bean对象。
这时候,我们通过修改需要被注入的对象变量的名称为注解@Repository中的value值时,发现终于可以注入成功,分别修改两次,然后运行结果如下:
由上述实验可总结:
自动注入时先匹配类型,若容器中有两个及其以上的bean跟要注入的对象类型一致的话,此时则根据要注入的对象变量的名字跟容器中的bean的id(即用于创建对象的那几个注解中的value值)比较,若名字和id相同则完成注入。
作用:在按照类中注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用(必须和@Autowired一起使用),但是在给方法参数注入时可以。
属性:
value:用于指定注入bean的id(即用于创建对象的那几个注解中的value值)。
使用@Qualifier注解后的效果如下:
此时,即使spring容器中有相同类型的bean,也不会再根据需要被注入对象变量的名字去匹配bean对象了,而是根据@Qualifier中指定的value值去注入相应id的bean对象。
作用:
通过指定bean的id值,直接根据bean的id注入。它可以独立使用,而不必跟@Autowired一起使用。
属性:
name:用于指定bean的id。
由上可见,当处理有多个类型相同的bean的对象的时候,使用了@Resource注解就不必同时使用@Autowired和@Qualifier了。
注:以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。
另外,集合类型的注入只能通过XML来实现。
作用:
用于注入基本类型和String类型的数据。
属性:
value:用于指定数据的值。它可以使用spring中SpEL(也就是spring的el表达式)
SpEL的写法:${表达式}
好了,到这里我大概介绍了Spring IoC框架中的部分注解,Spring框架中还有很多的其他注解,其具体的用法大家在审计代码的时候遇到了再到网上查询吧。
此时,即使spring容器中有相同类型的bean,也不会再根据需要被注入对象变量的名字去匹配bean对象了,而是根据@Qualifier中指定的value值去注入相应id的bean对象。
作用:
通过指定bean的id值,直接根据bean的id注入。它可以独立使用,而不必跟@Autowired一起使用。
属性:
name:用于指定bean的id。
由上可见,当处理有多个类型相同的bean的对象的时候,使用了@Resource注解就不必同时使用@Autowired和@Qualifier了。
注:以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。
另外,集合类型的注入只能通过XML来实现。
作用:
用于注入基本类型和String类型的数据
属性:
value:用于指定数据的值。它可以使用spring中SpEL(也就是spring的el表达式)
SpEL的写法:${表达式}
好了,到这里我大概介绍了Spring IoC框架中的部分注解,Spring框架中还有很多的其他注解,其具体的用法大家在审计代码的时候遇到了再到网上查询吧。
AOP(面向切面编程)
关于AOP的内容我在这里就不讲述了,想了解的可以自己在网上学习。