JavaEE--框架篇(1)Spring

目录

前言

框架篇(1)Spring

IOC

实现原理

自定义实现简单的IOC

Spring中的IOC

Spring IOC相关知识梳理

***:Scope的取值范围以及各自的含义

***:@Autowired注解的搜索规则是什么?

AOP

实现原理

自定义实现简单的AOP

Spring中的AOP

***:切点,切面,前置通知、环绕通知等是什么?

***:要想实现事务控制,为什么必须接管Dao实例以及数据库连接池实例?


前言

带着问题学java系列博文之java基础篇。从问题出发,学习java知识。


框架篇(1)Spring

经历过上篇博文《JavaEE--无框架开发后台服务,经历造轮子的痛苦》的纯手工开发后台服务的痛苦,相信肯定迫不及待使用框架来高效率开发后台服务,无需关注业务之外的处理逻辑。本篇博文带领大家使用框架开发后台服务,体验框架之美,从经典的后台服务框架SSM/SSH入门。SSM/H(Spring  Spring MVC Mybatis / Hibernate),本篇博文先讲第一个S(Spring)。

 

IOC

IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”,也有人叫做“依赖注入”。它的主要作用是用于代码解耦,由框架实现bean全生命周期的管理。未使用框架时,我们要用到一个对象实例时,都是由使用者new一个出来,而现在是由框架创建,使用者从框架容器中取用;使用者对对象实例的控制权转到了框架,所以叫做“控制反转”。对象实例的属性是另一个对象实例,未使用框架时,我们是通过setter方法设置属性值,而现在是由框架在实例化时,注入属性值,所以也叫作“依赖注入”。

 

实现原理

IOC的实现步骤主要为:读取配置文件/扫描注解,通过反射(clazz.newInstance())创建bean实例,实例化过程中,查看其属性是否依赖其他bean,如果有则先实例化依赖的bean,依次逐级实例化;然后再反向逐级设置属性值。Spring框架会将实例化的bean存储在容器(ConcurrentHashMap)中,实现对其全生命周期的管理。

 

自定义实现简单的IOC

在之前的博文《Java基础篇--反射和注解》中,我们从bean注解,到Autowired注解,逐步实现了自定义注解;扫描到注解后,通过反射创建实例,通过向field注入实例,发现依赖注入,则递归调用,完成整个链路的初始化,详细的范例代码可以回顾博文。

 

Spring中的IOC

上例自定义实现的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。使用标签配置bean实例,标签配置bean的属性值,标签配置bean的带参构造器的参数。

@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配置,也是为了让我们可以更好的理解注解。

 

Spring IOC相关知识梳理

bean注解
注解名 具体作用 使用位置
@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的取值范围以及各自的含义

scope范围限定
注解名 具体作用 使用位置
@Scope 配合bean注解一起使用,限定实体类的作用范围

Spring框架对接管的实例支持不同作用范围的设定,主要定义如下:

  • singleton:单实例,即在容器中始终只有一个实例;优点是最小的资源消耗,缺点是线程不安全。不设定scope时,spring框架默认使用该限定;
  • prototype:多实例,每次取用都是新建一个实例;优点是线程安全,缺点是最大的资源消耗。
  • request:一次请求一个实例,浏览器发起一次请求间,保持单一实例,互相共享;
  • session:一次会话一个实例,浏览器与后台服务之间是http协议通讯,经过3次握手,建立了连接通道后,浏览器与后台服务间就可以发起多次请求,这都算作一次会话(连接未断开都算做一次会话),一次会话间共享同一实例。
  • global-session:在web应用中,等同于session

***:@Autowired注解的搜索规则是什么?

属性注入注解
注解名 具体作用 使用位置
@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

AOP (Aspect Orient Programming),直译过来就是面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。Spring中AOP的主要使用是日志模块以及数据库操作事务控制模块。

 

实现原理

拿数据库操作事务控制举例,实现步骤是:扫描注解,实例化Dao实例,实例化过程中,继续扫描其方法上是否有@Transaction注解,有则对其进行动态代理增强。所以AOP的实现主要是:反射和动态代理。Spring中实现动态代理主要有两种方式:有父接口的子类,则使用jdk的api基于接口生成代理对象;没有父接口的实体类,则使用cglib基于子类生成代理对象。

 

自定义实现简单的AOP

按照上面的原理分析,接下来我们尝试简单实现一下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();
    }
}

执行结果如下图:

JavaEE--框架篇(1)Spring_第1张图片JavaEE--框架篇(1)Spring_第2张图片

可以看到对于Dao实例添加了@Transaction注解的方法,执行前会开启事务,执行后会提交事务,执行完毕,会释放资源;而未添加@Transaction注解的方法,则不做处理;当注解的方法发生异常时,可以捕获异常,触发回滚。

 

Spring中的AOP

上面范例实现的事务控制只是一个模拟,并没有真的实现事务回滚,当发生异常时,数据库也不会恢复成原来的状态。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类对所有匹配的方法进行增强。

***:切点,切面,前置通知、环绕通知等是什么?

上面两例引入了几个概念,下面我们一一解释下:

  • 切点:需要增强的方法,类比自定义事务控制范例,相当于UserInfoDao中使用@Transaction注解的transfer();
  • 切面:对切点进行动态代理,使用try-catch-finally包裹方法,进行具体增强的工厂类,类比自定义事务控制范例,相当于ProxyFactory;Spring框架做了封装,无需程序员再手动实现动态代理,只需要一个普通的类,编写好对应的通知方法即可。Spring框架会做好动态代理,在try-catch-finally体系对应的位置,调用该切面类的对应通知方法。
  • 前置通知:在切点执行之前执行,类比自定义事务控制范例,相当于打印“开始事务”;
  • 后置通知:在切点执行之后执行,类比自定义事务控制范例,相当于打印“提交事务”;
  • 异常通知:在捕获到切点执行时抛出的异常,catch方法体中执行,类比自定义事务控制范例,相当于打印“捕获异常,触发回滚”;
  • 最终通知:在try-catch-finally体系,finally方法体中执行,类比自定义事务控制范例,相当于打印“关闭连接,释放资源”;
  • 环绕通知:Sping框架提供的支持自定义增强方法,可以通过Spring封装好的ProceedingJoinPoint对象,拿到切点的参数、执行切点、拿到执行后的返回值等,然后由程序员自己实现具体的增强逻辑。类比自定义事务控制范例,相当于开放ProxyFactory的intercept(),支持用户自定义实现。

***:要想实现事务控制,为什么必须接管Dao实例以及数据库连接池实例?

我们实现的事务控制范例其实并没有真的实现事务控制,也无法回滚数据库操作,恢复数据库的状态。要想实现事务回滚,可以利用Connection数据库连接的系列方法,在切点开始操作数据库表之前,设置不自动提交事务(connection.setAutoCommit(false));在捕获到切点执行异常时,调用connection.rollback()回滚操作;当切点正常执行,则提交事务connection.commit();在最终finally方法体中执行connection.close()等资源释放。可以看到整个流程要求,代理对象拿到的数据库连接Connection和调用Dao实例方法拿到的Connection必须是同一个,否则事务控制将无法生效(两个connection不一致,操作也互不影响)。所以首先Spring框架要对Dao实例就行接管,这样才能在扫描到@Transaction注解时,对它进行aop动态增强;其次必须实现对数据库连接池的接管,因为这样才能确保切点获取的Connection和代理对象拿到的Connection是一致。因为Dao实例可能被多个线程调用,为了保证每个线程间使用的Connection是独立的,所以数据库连接池使用一个ThreadLocal来存储当前线程的Connection实例。

事务控制的具体实现大致如下:

/**
 * 连接工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
 */
@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();
        }
    }
}

以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!

你可能感兴趣的:(java,spring,ioc,aop)