目录
前言
框架篇(1)Spring
IOC
实现原理
自定义实现简单的IOC
Spring中的IOC
Spring IOC相关知识梳理
***:Scope的取值范围以及各自的含义
***:@Autowired注解的搜索规则是什么?
AOP
实现原理
自定义实现简单的AOP
Spring中的AOP
***:切点,切面,前置通知、环绕通知等是什么?
***:要想实现事务控制,为什么必须接管Dao实例以及数据库连接池实例?
带着问题学java系列博文之java基础篇。从问题出发,学习java知识。
经历过上篇博文《JavaEE--无框架开发后台服务,经历造轮子的痛苦》的纯手工开发后台服务的痛苦,相信肯定迫不及待使用框架来高效率开发后台服务,无需关注业务之外的处理逻辑。本篇博文带领大家使用框架开发后台服务,体验框架之美,从经典的后台服务框架SSM/SSH入门。SSM/H(Spring Spring MVC Mybatis / Hibernate),本篇博文先讲第一个S(Spring)。
IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”,也有人叫做“依赖注入”。它的主要作用是用于代码解耦,由框架实现bean全生命周期的管理。未使用框架时,我们要用到一个对象实例时,都是由使用者new一个出来,而现在是由框架创建,使用者从框架容器中取用;使用者对对象实例的控制权转到了框架,所以叫做“控制反转”。对象实例的属性是另一个对象实例,未使用框架时,我们是通过setter方法设置属性值,而现在是由框架在实例化时,注入属性值,所以也叫作“依赖注入”。
IOC的实现步骤主要为:读取配置文件/扫描注解,通过反射(clazz.newInstance())创建bean实例,实例化过程中,查看其属性是否依赖其他bean,如果有则先实例化依赖的bean,依次逐级实例化;然后再反向逐级设置属性值。Spring框架会将实例化的bean存储在容器(ConcurrentHashMap)中,实现对其全生命周期的管理。
在之前的博文《Java基础篇--反射和注解》中,我们从bean注解,到Autowired注解,逐步实现了自定义注解;扫描到注解后,通过反射创建实例,通过向field注入实例,发现依赖注入,则递归调用,完成整个链路的初始化,详细的范例代码可以回顾博文。
上例自定义实现的IOC还是非常简单的,而Spring中的IOC比我们自定义实现的完善多了,主要体现在:对bean的注解支持多种(@Controller,@Service,@Repository,@Component),对存放bean的key支持多种设定(name值,默认类名首字母小写,类名首字母连续大写的则直接使用类名),对属性注入支持多种搜索规则,对bean的作用范围支持多种设定等等。下面我们来一一学习下:
/**
* 用户信息表数据库操作类
*/
public class UserInfoDao {
//省略其它方法
}
/**
* 业务处理类
*/
public class UserService {
private UserInfoDao userInfoDao;
public void setUserInfoDao(UserInfoDao userInfoDao){
this.userInfoDao = userInfoDao;
}
//省略其它方法
}
public class ConstructParam {
private int id;
private String name;
private String sex;
public ConstructParam(int id, String name, String sex) {
this.id = id;
this.name = name;
this.sex = sex;
}
}
如上例,是使用bean.xml配置文件的方式实现IOC。使用
@Repository("userInfoDao")
@Scope("singleton")
public class UserInfoDao {}
/**
* 业务处理类
*/
@Service
@Scope("singleton")
public class UserService {
@Autowired
private UserInfoDao userInfoDao;
public boolean save(UserInfo userInfo){
return userInfoDao.save(userInfo);
}
//省略其它方法
}
@Configuration
public class ParamsConfig {
@Bean
@Scope("prototype")
public ConstructParam initParam(){
return new ConstructParam(1,"张三","男");
}
}
如上例,是使用注解的方式实现IOC。@Repository注解表明是一个Dao实例,@Service注解表示是一个Service实例,@Autowired注解注入属性值;另外,由于ConstructParam类仅有带参构造器,所以不能通过简单的在其类上加注解交给框架接管;为了配置这种带参构造器的bean,需要借助@Configuration注解,编写一个配置类,然后再搭配@bean注解方法,在方法中调用具体的带参构造器,实例化具体的bean,交给spring框架接管生命周期和作用范围。对比上面两例,需要注意,使用xml配置的方式,属性注入是调用bean实体类的setter方法实现的,所以bean实体类中必须要有该属性的setter方法;而使用注解的方式,属性注入是依赖反射拿到field,通过field.set()注入属性值,所以实体类中可以不用写该属性的setter方法。由原理分析也得出,bean实例化时是通过反射,调用默认的无参构造器创建实例(clazz.newInstance()),所以实体类必须要有无参构造器;如果仅有带参构造器,则必须在xml配置时配置具体的构造器参数,或者使用特殊的注解方式,确保框架可以明确调用具体的带参构造器(clazz.getConstructor(参数类型).newInstance())。
可以看到使用注解会使得代码简洁很多,注解也正是框架发展的主流,所以我们也主要关注注解的使用方式,上例使用xml配置,也是为了让我们可以更好的理解注解。
注解名 | 具体作用 | 使用位置 |
@Controller | 表示该类是一个Controller实体类 | 类 |
@Service | 表示该类是一个Service实体类 | 类 |
@Repository | 表示该类是一个Dao层实体类 | 类 |
@Component | 表示该类是一个组件实体类 | 类 |
@Configuration | 表示该类是一个配置类,搭配@bean使用 | 类 |
@Bean | 搭配@Configuration使用,作用于方法上,表明方法返回值是一个需要被框架接管的实体 | 方法 |
Spring框架遵循MVC架构,为了区分各个层面的实体,设计了多个bean注解(@Controller、、、@Component),其实这些注解都可以混用(作用都和我们自定义的@bean注解类似),不过为了代码的可阅读性,建议还是按照规范使用:Controller都用@Controller注解,Service都用@Service注解,Dao都用@Repository注解,其它找不到具体对应层面的都用@Component注解。Spring定义的这些bean注解,支持使用name或者value设定存储时的key(使用ConcurrentHashMap存储),如果没有设定,则默认使用类名首字母小写,如果类名首字母连续大写,则直接使用类名。
注解名 | 具体作用 | 使用位置 |
@Scope | 配合bean注解一起使用,限定实体类的作用范围 | 类 |
Spring框架对接管的实例支持不同作用范围的设定,主要定义如下:
注解名 | 具体作用 | 使用位置 |
@Autowired | 注入属性值 | 属性 |
@Qualifier | 搭配@Autowired使用,指定属性实例在容器中的别名(key) | 属性 |
@Resource | 可以选择使用name参数指定key,注入属性值 | 属性 |
@Value | 注入基本类型的属性值,搭配EL表达式使用,可取配置文件中的值 | 基本类型属性 |
我们自定义的IOC中@Autowired注解是默认使用属性名去搜索容器,查找具体的bean实例并返回。Spring框架中就完善多了,主要搜索规则如下:
1.使用@Qualifier指定了key,则使用该key去容器中搜索,找到则返回;
2.未指定key,则首先默认使用属性名去容器中搜索,找到则返回;未找到则继续搜索容器中是否有与属性同类型的实例,找到则返回;未找到同类型实例则继续搜索是否有同类型子类实例,找到则返回;
3.@Resource注解是javax.annotation包下,不是Spring框架的自定义注解,但是Spring框架也会扫描该注解,未使用name参数指定key时,则默认使用属性名去搜索;使用name参数指定了key,则使用该key去搜索。
***:小贴士-》如上分析,@Autowired注解支持多匹配规则,所以建议实际开发时使用@Autowired注解来注入属性;也要注意避免同名bean实例出现在框架的bean容器中,减少混淆,即尽量避免使用@Qualifier。
AOP (Aspect Orient Programming),直译过来就是面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。Spring中AOP的主要使用是日志模块以及数据库操作事务控制模块。
拿数据库操作事务控制举例,实现步骤是:扫描注解,实例化Dao实例,实例化过程中,继续扫描其方法上是否有@Transaction注解,有则对其进行动态代理增强。所以AOP的实现主要是:反射和动态代理。Spring中实现动态代理主要有两种方式:有父接口的子类,则使用jdk的api基于接口生成代理对象;没有父接口的实体类,则使用cglib基于子类生成代理对象。
按照上面的原理分析,接下来我们尝试简单实现一下AOP,事务控制模块;然后模拟银行转账,测试一下框架的功能。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Bean {}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Transaction { }
//自定义注解解析器
public class AnnotationLoader {
private static ConcurrentHashMap beanMap = new ConcurrentHashMap<>();
public static Object initBean(Class> clazz,String name){
Object bean = null;
try {
if (clazz.isAnnotationPresent(Bean.class)){
Object target = clazz.newInstance();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(Transaction.class)){
//生成动态代理对象
bean = new ProxyFactory(target, method.getName()).getProxyInstance();
}
}
//未指定name参数,则默认类型名首字母小写作为key,保存进beanMap
if (null == name || name.trim().length() == 0){
name = toLowerCaseFirstOne(clazz.getSimpleName());
}
beanMap.put(name,bean);
System.out.println("key:"+name+",bean:"+bean);
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return bean;
}
/**
* 对string字串的首字母进行小写转换
* @param s
* @return
*/
public static String toLowerCaseFirstOne(String s){
if(Character.isLowerCase(s.charAt(0)))
return s;
else
return (new StringBuilder()).append(Character.toLowerCase(s.charAt(0))).append(s.substring(1)).toString();
}
}
//动态代理工厂
public class ProxyFactory implements MethodInterceptor {
//被代理对象
private Object target;
//需要增强的方法名
private String methodName;
public ProxyFactory(Object target,String methodName) {
this.target = target;
this.methodName = methodName;
}
//给被代理对象创建一个代理
public Object getProxyInstance(){
//工具类
Enhancer enhancer = new Enhancer();
//设置被代理父类
enhancer.setSuperclass(target.getClass());
//设置回调
enhancer.setCallback(this);
//创建代理
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if (method.getName().equals(methodName)){
System.out.println("开启事务");
try {
Object invoke = method.invoke(target, objects);
System.out.println("提交事务");
return invoke;
} catch (Exception e) {
e.printStackTrace();
System.out.println("捕获异常,触发回滚");
} finally {
System.out.println("关闭连接,释放资源");
}
} else {
Object invoke = method.invoke(target, objects);
return invoke;
}
return null;
}
}
如上,我们自定义实现了一个aop事务控制框架,其中动态代理对象是依赖于cglib实现。接下来我们测试一下:
/**
* 业务处理类,使用自定义的bean注解,让框架IOC接管bean的生命周期
* 使用自定义的@Transaction注解,让框架对具体方法进行动态增强,实现事务控制
*/
@Bean
public class UserService {
public void save(){
System.out.println("向账户表添加一条记录");
}
@Transaction
public void transfer(){
System.out.println("从A账户扣除100元");
//手动制造异常
int i = 1/0;
System.out.println("向B账户存入100元");
}
}
/**
* 测试IOC和AOP
*/
public class TestTransaction {
public static void main(String[] args) {
UserService userService = (UserService) AnnotationLoader.initBean(UserService.class, null);
userService.transfer();
System.out.println("----------华丽的分割线------------");
userService.save();
}
}
执行结果如下图:
可以看到对于Dao实例添加了@Transaction注解的方法,执行前会开启事务,执行后会提交事务,执行完毕,会释放资源;而未添加@Transaction注解的方法,则不做处理;当注解的方法发生异常时,可以捕获异常,触发回滚。
上面范例实现的事务控制只是一个模拟,并没有真的实现事务回滚,当发生异常时,数据库也不会恢复成原来的状态。Spring中对AOP做了完善的模板封装,支持配置各种通知,支持使用execution表达式匹配多种切点,支持使用一个简单的类作为切面,程序员无需关注如何实现动态代理(Spring框架中对于有父接口的子类,是使用jdk的api进行基于接口动态代理;对于没有父接口的实体类,是使用cglib进行基于子类动态代理)。下面就日志模块举例,学习一下Spring的aop:
public class Logger {
/**
* 用于打印日志,计划在切入点方法执行前执行
* 前置通知
*/
public void beforePrintLog(){
System.out.println("Logger类中的beforePrintLog方法开始记录日志……");
}
/**
* 用于打印日志,计划在切入点方法执行前执行
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("Logger类中的afterReturningPrintLog方法开始记录日志……");
}
/**
* 用于打印日志,计划在切入点方法执行前执行
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("Logger类中的afterThrowingPrintLog方法开始记录日志……");
}
/**
* 用于打印日志,计划在切入点方法执行前执行
* 最终通知
*/
public void afterPrintLog(){
System.out.println("Logger类中的afterPrintLog方法开始记录日志……");
}
/**
* 用于打印日志,计划在切入点方法执行前执行
* 环绕通知
*/
public void aroundPrintLog(ProceedingJoinPoint pjp){
try {
pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Logger类中的aroundPrintLog方法开始记录日志……");
}
}
@Service
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("执行了保存");
int i = 1/0;
}
public void updateAccount(int i) {
System.out.println("执行了更新");
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
如上是使用xml配置具体切点,切面,通知的例子,实现自定义Logger类对匹配的切点方法进行动态增强(添加日志)。
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.zst.service.*.*(..))")
private void pt1(){ }
/**
* 用于打印日志,计划在切入点方法执行前执行
* 前置通知
*/
// @Before("pt1()")
public void beforePrintLog(){
System.out.println("Logger类中的beforePrintLog方法开始记录日志……");
}
/**
* 用于打印日志,计划在切入点方法执行前执行
* 后置通知
*/
// @AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("Logger类中的afterReturningPrintLog方法开始记录日志……");
}
/**
* 用于打印日志,计划在切入点方法执行前执行
* 异常通知
*/
// @AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("Logger类中的afterThrowingPrintLog方法开始记录日志……");
}
/**
* 用于打印日志,计划在切入点方法执行前执行
* 最终通知
*/
// @After("pt1()")
public void afterPrintLog(){
System.out.println("Logger类中的afterPrintLog方法开始记录日志……");
}
/**
* 用于打印日志,计划在切入点方法执行前执行
* 环绕通知
*/
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();
System.out.println("Logger类中的beforePrintLog方法开始记录日志……");
rtValue = pjp.proceed(args);
System.out.println("Logger类中的afterReturningPrintLog方法开始记录日志……");
return rtValue;
} catch (Throwable t) {
System.out.println("Logger类中的afterThrowingPrintLog方法开始记录日志……");
throw new RuntimeException(t);
} finally {
System.out.println("Logger类中的afterPrintLog方法开始记录日志……");
}
}
}
如上,是使用注解的方式配置,实现使用Logger类对所有匹配的方法进行增强。
上面两例引入了几个概念,下面我们一一解释下:
我们实现的事务控制范例其实并没有真的实现事务控制,也无法回滚数据库操作,恢复数据库的状态。要想实现事务回滚,可以利用Connection数据库连接的系列方法,在切点开始操作数据库表之前,设置不自动提交事务(connection.setAutoCommit(false));在捕获到切点执行异常时,调用connection.rollback()回滚操作;当切点正常执行,则提交事务connection.commit();在最终finally方法体中执行connection.close()等资源释放。可以看到整个流程要求,代理对象拿到的数据库连接Connection和调用Dao实例方法拿到的Connection必须是同一个,否则事务控制将无法生效(两个connection不一致,操作也互不影响)。所以首先Spring框架要对Dao实例就行接管,这样才能在扫描到@Transaction注解时,对它进行aop动态增强;其次必须实现对数据库连接池的接管,因为这样才能确保切点获取的Connection和代理对象拿到的Connection是一致。因为Dao实例可能被多个线程调用,为了保证每个线程间使用的Connection是独立的,所以数据库连接池使用一个ThreadLocal
事务控制的具体实现大致如下:
/**
* 连接工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
*/
@Component
public class ConnectionUtils {
private ThreadLocal tl = new ThreadLocal();
@Autowired
private DataSource dataSource;
/**
* 获取当前线程的连接
* @return
*/
public Connection getThreadConnection(){
Connection conn = tl.get();
try {
if (conn == null){
//从数据源获取连接
conn = dataSource.getConnection();
//把连接与线程绑定
tl.set(conn);
}
return conn;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 解除连接和线程的绑定
*/
public void removeConnection(){
tl.remove();
}
}
/**
* 和事务管理相关的工具类:开启,提交,回滚,和释放连接
* 相当于事务控制的切面类
*/
@Component
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
/**
* 开启事务
* 相当于前置通知
*/
public void benginTransaction(){
try {
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 提交事务
* 相当于后置通知
*/
public void commit(){
try {
connectionUtils.getThreadConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 回滚事务
* 相当于异常通知
*/
public void rollback(){
try {
connectionUtils.getThreadConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 释放连接
* 相当于最终通知
*/
public void release(){
try {
connectionUtils.getThreadConnection().close();
connectionUtils.removeConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!