Spring使用纪要

一、定时任务

1、定时任务的几种实现方式

1)TimerTask

Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行
因为它不能在指定时间执行, 仅仅能让程序依照某一个频度执行,所以TimerTask在实际项目中用的不多。
这里只列一个简单的示例,说明一下Timer的使用方式,不作深入探讨:

a、约定

需要: spring.jar, commons-logging.jar 两个包

b、配置
<?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-2.5.xsd">
	
	<bean id="taskService" class="com.zdp.service.TaskService"></bean>
	<bean id="scheduledTimerTask" class="org.springframework.scheduling.timer.ScheduledTimerTask">
		<property name="timerTask" ref="taskService" />
		
		<!-- 每隔一天运行一次配置: 24*60*60*1000 -->
		<!-- 每1秒钟程序运行一次  -->
		<property name="period" value="1000" />
		
		<!-- 程序启动4秒钟后開始运行  -->
		<property name="delay" value="4000" />
	</bean>
	
	<bean id="timerFactoryBean" class="org.springframework.scheduling.timer.TimerFactoryBean">
		<property name="scheduledTimerTasks">
			<list>
				<ref bean="scheduledTimerTask" />
			</list>
		</property>
	</bean>
</beans>
c、编码

实现方式就是继承TimerTask 类,重写run方法即可。

/**
 * 定时调度业务类
 */
public class TaskService extends TimerTask {
	
	@Override
	public void run() {
		String currentTime = new SimpleDateFormat("yyy-MM-dd hh:mm:ss").format(new Date());
		System.out.println(currentTime + " 定时任务正在运行...");
	}
}

2)(比较常用)Spring自带的task

Spring3.0以后自带的task,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。它的好处是,不需要额外引入jar包,配置比较简单,而且功能也比较全面。

3)Quartz

Quartz是一个功能强大的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行,但是配置起来稍显复杂。

2、应用实例

1)用sping提供的task实现定时任务(基于注解方式)

【配置】
在applicationContext.xml中作如下配置
配置组件扫描

<context:component-scan base-package="com.xxx.yyy.schedule">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>

开启task任务注解扫描

<task:annotation-driven/>

成功添加此配置后,applicationContext.xml的命名空间,会新增以下三项内容,注意检查有没有

xmlns:task="http://www.springframework.org/schema/task
http://www.springframework.org/schema/task 
http://www.springframework.org/schema/task/spring-task-4.1.xsd

如上是最简单的配置,可以满足基础的定时任务需求。

【编码】
在包com.xxx.yyy.schedule下(与配置base-package的值对应),新建定时任务类

@Component
public class WecomMarkTagExecutor {

    private Logger logger = LoggerFactory.getLogger(WecomMarkTagExecutor.class);

    @Autowired
    private WeComMarkTagService weComMarkTagService;

    @Scheduled(cron = "0 0/2 * * * ? ")
    public void task01() {
        try {
            logger.info("wecom打标签任务-start");
            weComMarkTagService.wecomMarkTag();
            logger.info("wecom打标签任务-end");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2)开发中遇到的问题

a、注意spring的组件扫描配置

添加包扫描比较简单,就像1)中示例的配置一样。
有时候,定时任务实现类所在包下、有些我们不想放入容器的类,如何配置能实现只把想放入容器的类放进去、不想放的不放进去呢?
这里说一个通过正则表达式不把指定类放入容器的配置,假设schedule包下有两个定时任务:EveryDayExecutor和WecomMarkTagExecutor,其EveryDayExecutor是其他模块的定时任务,当前模块不能将它放入容器,我们可以作如下配置

<context:component-scan base-package="com.haoys.bdp.*">
        <context:exclude-filter type="regex" expression="com.xxx.yyy.schedule.EveryDayExecutor"/>
</context:component-scan>

这个配置的意思是,扫满schedule包下的组件(放入容器),但排除EveryDayExecutor。

b、注意corn表达式的书写

corn表达式网上很多讲解,一定要注意检查自己写的对不对;我就是由于corn写错了、导致定时任务不能按预想的执行,以为配置错误,改来改去也改不好,不仅浪费时间、还特别影响心情。
注意cron = "0 0/2 * * * ? ",才是每隔2分钟执行一次;
cron = "0 2 * * * ? ",意思是每个小时的第二分钟执行!

c、注意配置

必须开启task注解扫描<task:annotation-driven/>,才能使@Scheduled注解被扫到,才能让定时任务生效。

2)为定时任务配置线程池

二、AOP

1、AOP的基础概念

2、实际应用中遇到的问题

1)aop与事务的执行顺序问题

a、问题描述

在使用aop时遇到一个这样问题:需求是用户信息更新后,要使用aop将更新后的信息,推送到其他系统;但是,实际使用时用户更新后,推送功能推的用户数据却是更新前的数据。

我们用的是@AfterReturning通知,按理说是在更新方法执行后才开始推送的,这时期望的是数据库中的用户信息已经更新,我们查出用户信息再推送、应该是最新的数据。

问题出在,更新方法要进行多个表的数据修改、使用了事务。所以,虽然更新方法先执行(更新用户和其他表)、aop通知方法后执行(查询用户和推送),但有些情景、比如数表中数据过多时,更新方法先执行但是在多个数据库修改操作没有完成前,事务不提交、表中用户数据不更新;这时aop查询方法“后发先至”,在用户数据更新之前先完成查询,就得到了错误数据。

b、解决方法

应该有很多解决方法,我这里是这样实现的:
首先,是否有和当前线程有关联的事务正在执行、是可以知道的,

boolean flag = TransactionSynchronizationManager.isActualTransactionActive();

spring提供的这个方法,当有相关的、正在执行事务时,返回true;
其次,TransactionSynchronizationManager提供了多个方法,可以让我们实现在事务提交过程的各个时间节点,执行我们的方法

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                    @Override
                    public void suspend() {

                    }

                    @Override
                    public void resume() {

                    }

                    @Override
                    public void flush() {

                    }

                    @Override
                    public void beforeCommit(boolean b) {

                    }

                    @Override
                    public void beforeCompletion() {

                    }

                    @Override
                    public void afterCommit() {
                        //执行事务提交后流程
                    }

                    @Override
                    public void afterCompletion(int i) {
                        //执行事务完成后流程
                    }
                });

所以我先判断当前线程是否有关联的事务正在执行?
如果有就在事务完成后再执行我的方法(事务提交后就执行应该就行),
如果没有,那就马上执行的方法。
注意:这些逻辑,要在aop通知方法中执行
具体代码如下

/**
 * @Description 用户数据推送
 **/
@Aspect
@Component
public class PushUserInfoAspect{

    private static ExecutorService threadPool = Executors.newFixedThreadPool(500);

    @Around("@annotation(pushDataToShop)")
    public Object around(ProceedingJoinPoint joinPoint, PushDataToShop pushDataToShop) throws Throwable{
        	//JSONObject param = getNameAndValue(joinPoint);
            String event = pushDataToShop.event();
            String dataPlace = pushDataToShop.value();
            JSONObject argObj = getNameAndValue(joinPoint);
            Object returnValue = joinPoint.proceed();
            JSONObject returnJsonOb = (JSONObject) JSONObject.toJSON(returnValue);
            //获取
            boolean flag = TransactionSynchronizationManager.isActualTransactionActive();
            //判断是否存在事务正在执行
            if(flag){
                //1.1 存在事务
                TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                    @Override
                    public void suspend() {

                    }

                    @Override
                    public void resume() {

                    }

                    @Override
                    public void flush() {

                    }

                    @Override
                    public void beforeCommit(boolean b) {

                    }

                    @Override
                    public void beforeCompletion() {

                    }

                    @Override
                    public void afterCommit() {
                        //执行事务提交后流程
                    }

                    @Override
                    public void afterCompletion(int i) {
                        try {
                            excutePush(returnJsonOb,argObj,dataPlace,event);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
            } else {
            //1.2 没有事务
                excutePush(returnJsonOb,argObj,dataPlace,event);
            }
            return returnValue;
    }

    /**
      * @Description 执行用户推送
     **/
    public void excutePush(JSONObject returnObj,JSONObject argObj,String dataPlace,String event) throws Exception{
        。。。
    }
}

补充:因为事务底层,也是通过aop实现的,而aop的执行顺序可以配置,所以还能通过直接配置事务与aop的执行顺序,解决上述问题。

3、应用实例(基于自定义注解)

1)应用场景

需求是:在a系统每次字典数据变更时,都需要给b系统同步一次数据,以保持两个系统字典数据相同。
字典的增、删、改、合并接口,都需要执行数据推送操作,如果不用AOP、这些接口都需要增加推送操作的代码,这样会大大增加主业务与推送的耦合度,而且改起来很麻烦、代码也很不优雅。
下面的应用实例

  • 我们使用AOP来降低耦合度;
  • 同时为了不影响主业务的执行效率,采用多线程异步执行推送;
  • 为了优雅,采用自定义注解标记连接点。

2)代码

a、约定
b、配置
c、自定义注解
@Target(ElementType.METHOD) /*注解作用范围*/
@Retention(RetentionPolicy.RUNTIME) /*注解生命周期*/
public @interface PushDataToB {
	/*加两个变量,方便业务功能拓展*/

    //数据类型:1=机构,2=字典
    String dataType() default "1";

    //方法类型:mm=合并
    //合并时给b系统两次,一次传删除的、一次传留下的
    String methodType() default "nmm";
}
d、写切面类
@Component
@Aspect
public class PushDataToBAspect {

    @Resource(name = "threadPool")
    private ThreadPoolTaskExecutor taskExecutor;

    /*@Pointcut("@annotation(com.hhh.bbb.annotation.PushDataToB)")
    public void annotationPointCut() {
    }*/

    @AfterReturning("@annotation(pushDataToB)")
    public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
        //获取参数
        String dataType = pushDataToB.dataType();
        String methodType = pushDataToB.methodType();
        Object argObj = getParams(joinPoint);
        //执行任务
        PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
        /*Thread t = new Thread(pushDataToBExecute);
        t.start();*/
        taskExecutor.execute(pushDataToBExecute);
    }

    //获取连接点参数
    private Object getParams(JoinPoint joinPoint) {
        /*Map<String, Object> param = new HashMap<>();
        //获取参数值数组
        Object[] paramValues = joinPoint.getArgs();
        //获取参数名数组
        String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
        //组装,返回jsonobject
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }
        return JSONObject.parseObject(JSON.toJSONString(param));*/
        //获取参数值数组
        Object[] paramValues = joinPoint.getArgs();
        return paramValues[0];
    }
}

代码基本可以分为以下几个部分:

@Pointcut

第一部分,是@Pointcut注解及其方法,用来定义切入点、即我们的通知要在什么地方执行;我这里用的注解标记,表示在用了@PushDataToB这个注解的地方,按照通知类型织入我们的通知。
其实,这个方法并不是必须的,因为通知注解也可以指明通知在什么地方执行。
那么@Pointcut在什么时候用呢?
当类中有多个方法定义的切面相同时,可以单独定义一个方法,不必写方法体,统一设置@Pointcut;当单独指定了一个方法设置好@Pointcut后,类中的其他方法如果要跟单独指定的方法设置相同的切面时,只需要引用设置@Pointcut方法的名字即可。所以上面的示例代码还可以是以下形式的

@Component
@Aspect
public class PushDataToBAspect {

	...

    @Pointcut("@annotation(com.hhh.bbb.annotation.PushDataToB)")
    public void annotationPointCut() {
    }

	@Before("annotationPointCut()")
    public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
        xxx方法代码
    }

    @AfterReturning("annotationPointCut()")
    public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
        yyy方法代码
    }

	...
    
}

可以看到代码中我给注掉了,因为我的需求只要一个通知方法就能实现,不需要公用切入点(而且即使公用也不是必须的)。

通知方法

第二个部分是通知方法

	@AfterReturning("@annotation(pushDataToB)")
    public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
        //获取参数
        String dataType = pushDataToB.dataType();
        String methodType = pushDataToB.methodType();
        Object argObj = getParams(joinPoint);
        //执行任务
        PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
        /*Thread t = new Thread(pushDataToBExecute);
        t.start();*/
        taskExecutor.execute(pushDataToBExecute);
    }

五类通知对应五类通知注解

前置通知(@Before):在目标方法运行之前运行
后置通知(@After):在目标方法运行结束之后运行(无论方法正常结束还是异常结束)
返回通知(@AfterReturning):在目标方法正常返回之后运行
异常通知(@AfterThrowing):在目标方法出现异常以后运行
环绕通知(@Around):动态代理,手动推进目标方法运行(joinPoint.procced()

可以在通知实现方法中增加JoinPoint类型的参数,用以接收实际业务(连接点方法)的方法名称和参数列表等信息,举例说明一下:
下面代码一个字典新增方法的实现方法,按照需求、字典新增我们要把数据推送到B系统,于是我们在方法上加自定义注解

	@PushDataToB(dataType = "2")
	@Override
    public void insertSelective(Dictionary dictionary) {
        xxx新增字典的实际代码,校验、新增等
        dictionaryDAO.insertSelective(dictionary);
    }

在切面类通知方法中,我们需要获取入参dictionary,所以通知方法如下:
我们使用最终通知@AfterReturning,因为这是一个新增,在新增执行成功前,没有字典id等数据,所以选择最终通知、在字典插入方法执行成功后,再获取参数、执行推送任务。

	@AfterReturning("@annotation(pushDataToB)")
    public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
        //获取参数
        String dataType = pushDataToB.dataType();
        String methodType = pushDataToB.methodType();
        Object argObj = getParams(joinPoint);
        //执行任务
        PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
        taskExecutor.execute(pushDataToBExecute);
    }

dataType、methodType是注解自定义的参数,如果需要正常获取就行;

JoinPoint

argObj参数是通过JoinPoint参数、以及动态代理实现的,具体代码如下:

	//获取连接点参数
    private Object getParams(JoinPoint joinPoint) {   
        //获取参数值数组
        Object[] paramValues = joinPoint.getArgs();
        return paramValues[0];
    }

因为示例中的主业务方法只有一个参数,所以我们直接获取第一个参数值就满足需求了;如果主业务方法有多个参数时,也可以获取,可以这样:

	//获取连接点参数
    private JSONObject getParams(JoinPoint joinPoint) {
        Map<String, Object> param = new HashMap<>();
        //获取参数值数组
        Object[] paramValues = joinPoint.getArgs();
        //获取参数名数组
        String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
        //组装,返回jsonobject
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }
        return JSONObject.parseObject(JSON.toJSONString(param));
    }

上面是得到JoinPoint以后,通过它获取主业务参数的代码,有一点需要注意
无论通知实现方法有多少个参数,JoinPoint类型的参数必须是第一个参数,否则Spring将无法识别。

在这里插入图片描述

ProceedingJoinPoint

还有一个ProceedingJoinPoint类,它继承了JoinPoint,而且添加了proceed()等方法,proceed()方法在环绕通知时会用到,通过它可以控制连接点方法的执行时机,即可以主动控制环绕通知方法的代码,哪些在连接点方法执行前要做,哪些在连接点方法执行后要做,应用示例如下:

@Around("@annotation(pushDataToB)")
public Object around(ProceedingJoinPoint joinPoint, PushDataToB pushDataToB) {
   //获取参数
   String event = pushDataToB.event();
   String dataPlace = pushDataToB.value();
   JSONObject argObj = getNameAndValue(joinPoint);
   
   Object returnValue = joinPoint.proceed();
   
   //
   JSONObject returnJsonOb = (JSONObject) JSONObject.toJSON(returnValue);
   excutePush(returnJsonOb,argObj,dataPlace,event);
   return returnValue;
}

上面代码是一个环绕通知方法,前三行代码是在主业务方法(也就是连接点方法)执行前执行的,最后三行代码是在主业务方法执行后执行的,这个功能在有些应用场景下非常好用。

e、多线程异步调用
在applicationContext.xml中配置一个线程池
	<!-- 线程池配置 -->
    <bean id="threadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <!-- 核心线程数  -->
        <property name="corePoolSize" value="5" />
        <!-- 最大线程数 -->
        <property name="maxPoolSize" value="30" />
        <!-- 队列最大长度 >=mainExecutor.maxSize -->
        <property name="queueCapacity" value="500" />
        <!-- 线程池维护线程所允许的空闲时间 -->
        <property name="keepAliveSeconds" value="200" />
        <!-- 线程池对拒绝任务(无线程可用)的处理策略 -->
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
        </property>
    </bean>
推动任务放在run()方法中

写一个通知执行类,实现Runnable,将通知任务(也就是推送业务)放在run()方法中

public class PushDataToBExecute implements Runnable{

    private static Logger logger = LogManager.getLogger(PushDataToBExecute.class.getName());

    //服务器地址
    private  String bServerUrl = PropertiesSource.getProperty("B_SERVER");

    private  String headPicHost = PropertiesSource.getProperty("HEAD_PIC_HOST");

    private Object data = null;

    private String dataType;

    private String methodType;

    @Override
    public void run() {
        try {
            //判断是否是合并操作,合并推送两次:一次传删除的、一次传留下的
            if ("mm".equals(methodType)){
				xxx代码
            }else {
                String pwd = Md5Utils.hash("xxx");
                Map<String, String> params = new HashMap<String, String>();//请求参数集合
                params.put("type", dataType);
                params.put("pwd", pwd);
                params.put("json", JSONObject.toJSONString(data));
                logger.info("Thread:{},推送数据到B系统Param :{}", Thread.currentThread(), JSONObject.toJSONString(params));
                String res = HttpUtils.doPostWithJson("http://xxxx", params);
                System.out.print(res);
            }
        }catch (Exception e){
            logger.error("Thread:{},推送数据到B系统出现错误:{},", Thread.currentThread(), e);
        }
    }

    public PushDataToBExecute(Object data, String dataType, String methodType){
        this.data = data;
        this.dataType = dataType;
        this.methodType = methodType;
    }
}
在切面类注入线程池
	@Resource(name = "threadPool")
    private ThreadPoolTaskExecutor taskExecutor;
异步执行推送任务
 	@AfterReturning("@annotation(pushDataToB)")
    public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
        
        xxx代码
        
        //异步执行推送任务
        PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
        taskExecutor.execute(pushDataToBExecute);
    }

X、工作中遇到的问题汇总

1、java.sql.SQLException: connection holder is null

1)问题描述:
对于大表进行查询、修改操作时,有时sql需要执行很长时间,这时就可能在执行到半路时、报错SQLException: connection holder is null
意思是,连接数据库的对象为null,就是连接断开了、过期了、没了。
2)原因:
在spring配置数据源时,一般都会配置以下两项

<!-- 是否自动回收超时连接 -->
<property name="removeAbandoned" value="true" />  
<!-- 超时时间,单位:秒(s) -->
<property name="removeAbandonedTimeout" value="10" />

意思是,一次对数据库的连接,当它超过设定时间时,spring就给他断开。
第一个是配置开不开这个功能;
第二个是配置限定时间。
3)解决:
方案一:

<!-- 直接关闭这个   自动回收超时连接 -->
<property name="removeAbandoned" value="false" />

方案二:延长超时时间

<!-- 是否自动回收超时连接 -->
<property name="removeAbandoned" value="true" />  
<!-- 超时时间,单位:秒(s) -->
<property name="removeAbandonedTimeout" value="1800" />

方案三:
关闭自动回收,然后再代码中手动设置回收。
这个我没用过
4)心得
这个回收还是有必要的,过长时间的sql连接,会影响其他功能、让系统变卡;严重的,还会耗尽数据库内存,导致数据源奔溃、系统停用。
但是有时候,又会有那种大批量的数据,需要统一处理;这种情况,建议把生产数据导到线下一份,在本地执行完成后、再把结果更新到生产,一定要保证生产环境稳定安全运行。
在本地执行的过程,可以暂时把“超时回收”关闭 或者 延长超时时间,执行完再改回来就行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值