一个SSM框架的整体写法:
所有的配置文件都放在WEB-INF包下面:
ApplicationContext.xml:
导入四个xml文件,分别为:
服务治理框架:
applicationContext-framework.xml applicationContext-db.xml
Business层织入AOP: applicationContext-aop.Xml
quartz调度: applicationContext-quartz.xml
ApplicationContext-framework.xml:
配置组件扫描器,使用注解方式开发
1:利用context标签下的component-scan组件扫描方法,扫描系统包下的注解,其中通过exclude-filter 过滤掉框架@Controller注解。
2:利用cmbus标签扫描并且装载该系统服务,同样利用cmbus扫描需要暴露的服务同样过滤框架的controller注解。
3:mvc:annotation-driven标签下的mvc:message-converters
代表着实现了httpmessageconverters接口,可以解析@requestbody和@responsebody注解代表的值。
mvc:annotation-driven标签提供enable-matrix-variables="true"属性.
该属性默认是关闭的(false),手动开启后,我们可以处理URL地址带有矩阵变量的请求
总的来说上面的标签的作用就是解决请求和返回时解析json数据用的。
4:
Bean下的property代表用set方式依赖注入,constructor-arg标签代表用构造方法依赖注入。 注入的都是父子依赖关系。
property: 它是bean标记的子标记,用以调用Bean实例中的相关Set方法完成属性值的赋值,从而完成依赖关系的注入
创建对象的协作关系称为装配,也就是DI(依赖注入)的本质
https://www.cnblogs.com/yusuf/p/12837005.html
2的原因见:
https://blog.csdn.net/gladmustang/article/details/39999721
为什么在ApplicationContext.Xml中的component-scan不扫描@controller而在dispatcher-servlet.xml中只扫描controller注解:
从官方文档可知context和servlet之间是父子关系。如果在两个地方同时扫描注解,则会在两个context空间生成bean,完全相同名字的bean。而框架会优先controller扫描servlet的空间进行service bean使用。如果你配置一个事务管理器aop,在context里,则系统扫描的时候扫描不到,因为你在servlet里没有配置,所以,一般在context文件中配置不扫描controller,在servlet只扫描controller,这样不会重复,而且不会导致扫描失败。
表示启动spring的组件扫描功能(从spring2.5版本开始)。即Spring容器初始化时,扫描base-package包或者子包下面的Java文件,如果扫描到有@controller、@Service、@Repository、@Component等注解的java类,就会将这些bean注册到工厂中 (纳入Spring容器管理)。还可以使用分号来分隔多个扫描包。
在类上,使用以下注解,实现bean的声明:
@Component:泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。
@Controller 用于标注控制层组件(如springMvc的controller,struts中的action)
@Service 用于标注业务层组件
@Repository用于标注数据访问组件,即DAO组件
在类的成员变量上,使用以下注解,实现属性的自动装配
@Autowired :按类的类型进行装配
@Resource:
1.如果同时指定了name和type,那么从Spring上下文中找到唯一匹配的bean进行装配
2. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常
3.如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常
4.如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;
如果在配置文件中配置了
https://blog.csdn.net/originations/article/details/88843698
三:applicationContext-aop.xml:
Aop主要对安全、事物、日志进行统筹管理:
bean注入aop切面类,对记录日志类进行注入,对business层进行aop设置。
通过aop:config proxy-target-class = “true”打开代理类,也就是business层下的类,对这些类进行切入点(
说一下:aop机制的实现都是通过aop框架生成的代理机制完成的。
Business层日志的写法:
传入切入点,也就是business层下的类。
定义开始、结束时间、请求、返回、异常、请求IP、启动日志=true等初始化信息。
try代码块:
获取接口配置,因为在business实现类的方法上面加入了@interfacelog注解,所以写一个方法getInterfaceLog(pjp),利用Method.getAnnonation的方式获得注解信息。该注解是为一些必要的字段赋初始值或者你在写注解的时候赋值,
然后判断是否启动是否为空,不为空且日志启动之后,进行正式操作。
把注解上的值(简单日志,描述,关键字等信息)拿到,然后判断是否是定时调度任务,如果是:
那么就把方法描述为定时调度任务,然后执行ProceedingJoinPoint.proceed的方法,也就是执行真正的business层的接口任务,获得一个返回值,然后new一个结束日期。构造下返回结果,因为是环绕通知嘛,就把这个返回结果返回给调用端。
如果不是定时任务,就把关键字,请求ID(这是封装在公共util里的方法,通过截取httpServeletRequest的请求头的第一段获得的),描述什么的拿到,然后同上执行接口,构造返回。
如果是异常信息,那么就构造异常返回报文(返回码、返回结果、时间、异常信息)。然后返回给调用端。
最后有一个finally方法的执行,就是当日志启动开关打开的时候,用logUtils的写方法,把日志落库(请求ID,描述,关键字,时间,异常,返回等等)
落库过程:
1:把请求和返回的基类值json序列化后放到log实体里面。
2:如果异常不为空的话,就给log的异常字段赋值异常和堆栈信息。
(先判断是否是CustomizeException,这个异常是一个格式化异常的公共类,重写了异常信息和堆栈信息。)
3:读取配置中心,把列表中有的方法名的调用时产生的日志发送指标到监控平台,用metricClient.send方法。
4:调用异步落库方法。
异步落库:
1:读取配置中心,属于非记录列表的funcname就不记录了( “NORMAL_NOTICE”,
“WARNING_E”, “WARNING_B”)
2:然后通过线程池执行excute方法去执行插入操作(因为日志记录很多情况是多线程并发的)。
3:线程池是自定义的,写法是线程池类里面写了一个内部静态类,用来单例获得线程池(因为不光是business层日志会用到这个线程池),获得的方法是Executors.newFixedThreadPool(线程池大小,默认是15).
4:插入的实现类,先对请求和返回的长度限制,如果是大于4000则大于的部分再次大于4000则不记录。截成的两段作为两个字段来插入。异常信息一样。
5:异步的实现是用线程池分发任务来实现的,从而使得cpu资源得到充分的利用。
日志落库的目的:
1:用来排除故障,便于查阅。
2:用来监控,有一套监控系统,从库里更新数据,如果某个接口的响应时间超过3000毫秒的话,会触发监控系统的警告,然后排查。
关于库满的情况,这里面有一个轮询机制,日志库一共有十三个,代表十二个月和一个备用的,每月用哪一个月的表,保留最近三个月的表信息。
用到了线程池,单例,aop,自定义注解,公共异常类,字符串截取等。
同理对sql慢查询和controller层的日志记录只是设置的标签不太一样,其他都是调用的公共的writelog方法。
自定义注解的定义以及使用:
我们要获取某个方法的调用日志,可以通过AOP(动态代理机制)给方法添加切面,通过反射来获取方法包含的注解,如果包含日志注解,就进行日志记录。
自定义注解:
用@interface修饰注解方法,该方法1:不能包含参数,2:返回值有局限,限于字符串、枚举、原始类型以及注解、或则这四种类型组成的数组。3:可以用default设置默认值。4;可以有四个注解绑定@Documented:类型声明@target,事件类型为方法或则类@retention:runTime表示运行时有效。source表示编译时有效。 有时会和java内置注解配合使用:@overide重写 @decrecate销毁注解@SuppressWarnings 忽略他们产生特殊警告。
以上是特性,下面是使用:
定义注解方法后,把注解放到想要被注解的方法上,然后就可以生效。同时可以读取注解,写一个读取注解的方法用method.getAnnotation(注解.class)
https://www.cnblogs.com/tartis/p/9299418.html
四:applicationContext-db.xml:
1:配置数据源:这里用的方法是bean注册引用jndi.JndiObjectFactoryBean,然后配置jndiname:为数据库库名。里面的具体的配置是写在tomcat的context文件里面的。比如sit环境中的最大活跃数:5(表示最多同时连接5个数据库),最大空闲数:1(表示即使不用,也会有1个连接待命,随时使用),最大连接时间15000ms(默认是9秒超时),以及url,用户名,密码,jdbcdriver等等。
2;使用 MapperScannerConfigurer,它 将 会 查 找 类 路 径 下 的 映 射 器 并 自 动 将 它 们 创 建 成 MapperFactoryBean,MapperFactoryBean可以创建代理类代理dao层接口,注入到应用中。然后是SqlSessionFactoryBean,MapperScannerConfigurer的自动装配可以不用去制定SqlSessionFactory。
在SqlSessionFactoryBean配置dao层xml路径来作为映射文件。
https://www.jb51.net/article/157618.htm
3:最后是transactionManager事务管理器
事务基础知识:
1:隔离级别:
多个事务同时运行可能会引起的情况。
(1):脏读:有1,2两个事务,1在操作完数据后比如-100,还未提交,2就读到了1提交后的数据
(2):不可重复读:有1,2两个事务,1多次对一个数据进行查询,查出来的数据不同,2在这期间对数据进行了更新
(3):幻读:有1,2两个事务,1查询数据时,2并发插入了记录,产生幻读。
默认级别:mysql:可重复读,sqlsrever和oracle提交读。
未提交读:隔离级别很低,不能防止脏,幻,不可重复读。
提交读:允许读取已经提交的事务,可防止脏读。
可重复读:对相同字段的多次读取结果一致,可以防止不可重复读,脏读。
串行化,逐一执行,锁定行和表,一般不用,可防止所有意外。
2:事务传播行为:
Nevers: 该方法不能在事务中运行,如果一个事务正在运行,则抛出异常。
Supports:如果一个事务正在运行,可以在事务中运行
Not_Supports:如果一个事务正在运行,该方法被挂起。
Requires:如果一个事务正在运行,该方法在事务中运行,否则开启新事务
Requires_New:新的事务被开启。
3:事务超时:事务提交前超时,则回滚。
4:事务只读属性:开启这个属性,后端数据库可以根据该属性做出相应的优化措施。
5:回滚规则:如果在事务中抛出了未检查异常,则回滚事务。未出现异常和一检查异常仍提交的事务不回滚。
我的xml文件的配置就是简单的获取事物管理器
然后再开启注解驱动事务管理器。在需要使用事务的方法上加上@transaction注解就好了。
声明式事务管理有两种方式,一是基于tx标签和aop标签来实现,第二是根据上面的注解来实现的。其本质是建立在aop之上的,就是在方法执行前创建或者加入一个事务,在方法执行结束之后根据执行情况提交或则回滚事务。
比如:
(批量)更新主表和入库表(事务)
@Transactional(rollbackFor = Throwable.class)
在捕获到这个异常的时候事务回滚。
在我们的代码里面一般是对同时更新和插入两张表以上的操作进行事务回滚设置,由于sqlserver的隔离级别是提交读,所以可以防止脏读。然后导致回滚的异常数组是Throwable。
Mybatis文件就是配置连接池数据源,把数据源加入到mybatis容器中,然后配置和spring集成的关系,去扫描spring框架下的包,然后把dao层包的类名映射到sqlsession工厂,从而使得spring和mybatis以及数据库三则建立一个连接关系。
事务的整体流程:
1:从数据源(Mybatis配置的datasource)获得一个事务管理器,也就是和底层数据库创建一个连接。
2:判断事务属性,是否超时,传播行为等。
3:开启事务,事务是与底层数据库绑定的,一个事务对应一个连接。
4:把上面的数据封装到一个对象(TransactionInfo)里,然后绑定到当前线程就好了。
Quarz调度任务的配置:
通过bean标签把schedule注入到spring容器中,然后把触发器和job任务注册到schedule调度容器内。触发器配置一个执行时间间隔,任务的话配置相应的java实现类。
为在Java应用程序中进行作业调度提供了简单却强大的机制。
1.持久性作业,就是保持调度定时的状态;
2.作业管理,对调度作业进行有效的管理;
3.类Corn的定时支持,可以用Corn的方式来执行作业;
4.线程处理模型 Timer是单线程作业的,但是Quartz支持线程缓冲池。
在Spring中可以很方便的使用Quartz来实现定时任务等功能:
首先是Schedule(任务调度器),Job(作业任务)和Trigger(触发器)三者的关系:
Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。
JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。
Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等。
Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。
Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。可以通过Scheduler# getContext()获取对应的SchedulerContext实例。
例子:
“30 10 1 * * ?” 每天1点10分30秒触发任务
“30 10 1 20 * ?” 每月20号1点10分30秒触发任务
“30 10 1 20 10 ? *” 每年10月20号1点10分30秒触发任务
“30 10 1 20 10 ? 2011” 2011年10月20号1点10分30秒触发任务
“30 10 1 ? 10 * 2011” 2011年10月每天1点10分30秒触发任务
“30 10 1 ? 10 SUN 2011” 2011年10月每周日1点10分30秒触发任务
“15,30,45 * * * * ?” 每15秒,30秒,45秒时触发任务
“15-45 * * * * ?” 15到45秒内,每秒都触发任务
“15/5 * * * * ?” 每分钟的每15秒开始触发,每隔5秒触发一次
“15-30/5 * * * * ?” 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
“0 0/3 * * * ?” 每小时的第0分0秒开始,每三分钟触发一次
“0 15 10 ? * MON-FRI” 星期一到星期五的10点15分0秒触发任务
“0 15 10 L * ?” 每个月最后一天的10点15分0秒触发任务
“0 15 10 LW * ?” 每个月最后一个工作日的10点15分0秒触发任务
“0 15 10 ? * 5L” 每个月最后一个星期四的10点15分0秒触发任务
“0 15 10 ? * 5#3” 每个月第三周的星期四的10点15分0秒触发任务
逐个派件任务:
1:通过枚举获得线程类型,线程id赋值为0.从配置中心调取任务管理类型其中封装线程名字(一共是处理鹰眼自动派件、处理鹰眼逐个派件和顺序派件),超时时间(10s),睡眠时间id和是否启动(true)。
2:去获得线程控制{ 判断是新建线程还是在已有线程上去上锁处理案件。如果是新建线程,那么新建后直接退出。如果已有线程,那么接着处理即可。
判断方式:
从数据库去拿线程实体:库里的线程实体就是逐个派件(5秒一次)和顺序派件(1分钟一次)主要是根据类型。
}
判断结果:
不存在,创建线程:
如果不存在,连同本机IP插入到库里的实体,返回插入成功与否。(模拟线程抢占成功)
然后再根据id和类型判断该线程数量,(你插入的时候返回了一个数量sum,根据id和类型判断数量sum1的时候让库里的sum《你传入的sum来判断,为0,说明你插入的过程中没有被抢占)这里是如果sum1=0的时候为没有被抢占,这属于双重加锁。
如果不等于0,就删除该线程。
存在:
如果实体已经存在,返回true(类似于线程加锁)。然后用update方法更新线程状态去加锁,更新成功加锁成功,失败则判断该线程是否超时处于停滞状态,如果是则
调用另一个更新方法,释放线程。
上锁成功:
调用配置中心的接口(逐个派件任务):这个时候就没有分布式的问题了,所以就可以在该线程内执行正常任务了。
1:通过配置中心配置的本地的路径去找到处理调度任务的controller。
2:调度任务开始,先根据业务类型和sessionId尝试业务乐观锁,
/dispatch/oneByOneDispatchOrderQuartz.json
重点:
这一部分的锁是基于数据库乐观锁去实现的分布式锁(有三种吗不是,这是基于数据库的)。
这不是物理意义上的锁,属于作业锁,猎风者系统一共有6个服务器,会同时执行,所以必须要搞这个。(业务中的定时任务的调度,主要是先抢占线程然后再抢占锁,获得锁的开始执行任务,这里以后都用总行lu48的预处理打标逻辑来说明)
有quarz引出的三种分布式锁的实现以及优缺点,我们的业务乐观锁和他们三个的比较:
实现并发控制的时候,就是去杜绝脏读,幻读,不可重复读的发生,主要实现方式有
悲观锁和乐观锁:
其中悲观锁就是:
在数据读取或者写操作的之前,把数据直接锁住,具有排他性和独占性,同时也能保证数据的绝对一致。悲观锁一般是基于数据库来实现的,比如行锁,表锁,读锁,写锁等。或则是synchronized 这个同步锁。
优点:数据高度安全
缺点:容易堵塞,性能低,并行差。
另外悲观锁包括了共享锁和排它锁,分别是读锁和写锁。共享锁是多个事务可以同时获取该锁,一般用于读数据。
悲观锁的具体实现:如果是mysql或则sql server的话,由于默认的是自动提交,所以要先把自动提交关闭属性等于0就行,oracle默认手动提交的。 然后进行实现:
其实就是:1:开启事务,2:查询数据后面用for update结尾(默认的行级锁),然后更新数据,4:提交事务。值得注意的是,行级锁是有索引触发的,所以你的查询要走索引查询,否则就会变成表级锁,效率下降更大。
其中乐观锁就是:
乐观锁是相对悲观锁来说的,一般认为在数据处理的过程中不会有数据其他操作的改变。所以在提交的时候才去判断数据读出的是否是最新且正确的。
Java包中的cas的实现是根据乐观锁实现的,另外数据库是在数据里加入一个version,每次数据更新就会把version+1;然后在提交的时候去判断该version是否相等,相等则提交。
实现:
它不需要借助数据库的锁机制,而是根据版本控制的。
finalAuditVincio:定时容器里的方法
dispatch/preProcessManagementByQuartz.json;分布式的接口
做分布式锁之前,先有线程的抢占:
(https://www.jianshu.com/p/d2ac26ca6525)这个链接里有大部分锁的介绍。
看到了乐观锁的实现记忆cas,下次再看记得看同步锁。
Java利用反射获取方法并执行:
①找到类:Class cl = Class.forName(“java.util.Date”);//获取类信息
②创建对象(要求这个类中含有空参数的构造方法):Object obj = cl.newInstence();
③根据名称和参数找方法:Method method1 = cl.getMethod(“getTime”);//如果没有参数不用写第二个参数如果有参数的如:Method method2 = cl.getMethod(“setTime”,long.class);
④在某个对象上面执行方法,Object result = method1.invoke(obj);//如果有参数的Object result = method2.invoke(obj,21317362721);