# spring4.x_web **Repository Path**: mjlfto/spring4.x_web ## Basic Information - **Project Name**: spring4.x_web - **Description**: 学习spring拉取其源码第2章 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2018-08-10 - **Last Updated**: 2020-12-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # spring4.x_web #### 项目介绍 一直都是使用spring boot 做restful服务, 今天突然看到用单纯的spring mvc配合jsp做web应用,感到还有几的陌生。不过现在用这样的模式应该少了吧,将前端展示和后台业务放到一起,首先web开发工程师和服务器开发工程都需要在同一份代码上操作,而且很多web开发工程师不会jsp,其次是安全, 将业务服务直接暴露出来是很安全的。如果使用spring boot 做后台业务服务, 再使用nginx做代理,前端代码直接放到nginx中,这样维护方便,而且实现了前后端分离,只要定义好前端与后台交互的接口文档即可。 使用传统spring mvc做web简单应用,这是一个最简单的mvn,用户首先进入登录页,然后输入用户名密码,点击登录后进入到欢迎页,每进入一次都会有积分奖励,同时后台会记录每次用户请求登录的ip地址与时间。 #### 软件架构 ![输入图片说明](https://images.gitee.com/uploads/images/2018/0810/120521_60c9724e_1665779.png "springmvc流程图.png") 盗用一张别人的spring mvc工作流程图,以下做说明: 1. 用户在浏览器中输入url后,后台服务交给DispatcherServlet做任务转发,每个请求该交到哪一个具体的controller,都是由它来控制,那为什么知道是由DispatcherServlet转发,这个是在web.xml中配置的.在配置中会发现需要去读取classpath:smart-context.xml配置文件中的内容,这个文件是spring的基本配置文件 2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回。 3. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter 4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 5. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象; 6. 根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet 。 7. ViewResolver 结合Model和View,来渲染视图 8. 将渲染结果返回给客户端。 #### 配置说明 1. web.xml放在webapp/WEB-INF/下,其中定义的DispatcherServlet servlet,同时定义了什么样的请求会被DispatcherServlet处理(这里定义为所有后缀为html的请求都会被DispatcherServlet处理) 2. 在web.xml中的定义servlet为smart,所以spring mvc配置文件则必须为(smart-servlet.xml),而且必须放在/webapp/WEB-INF/下,这个配置文件不需要通过web.xml中contextConfigLocation上下文参数进行声明 3. 在web.xml中通过contextConfigLocation上下文参数进行声明有一个spring配置文件(smart-context.xml),这个配置配件配置了那些类需要注册到ioc总,哪些地方需要aop拦截,还有一些其他资源配置和国际化配置,注意这个文件存放的位置,否则可能会出现容器找不到的情况 4. 日志配置文件(log4j.properties)这个文件是日志系统自行引用的,只要文件内容格式和位置存放没有问题就行 #### 使用说明 这个实例是通过Intellij(IEDA)做的,maven工程,可在工具右边找到有(Maven Projects)项目,运行可以找到jetty下的jetty:run命令(双击可执行),打包可以使用war下的war:war命令(双击可执行) #### 参与贡献 本实例是取至精通spring4.x----企业应用开发实战第二章3用例,完全用于学习参考,同时在这个实例中没有登录拦截这项功能,后续补齐 ![输入图片说明](https://images.gitee.com/uploads/images/2018/0810/145944_29e4b58e_1665779.png "package.png") #### applicationContext 生命周期说明 ![输入图片说明](https://images.gitee.com/uploads/images/2018/0811/151956_04426665_1665779.png "Applicationlife@2x.png") ``` //bean package com.smart.test; import org.springframework.beans.BeansException; import org.springframework.beans.factory.*; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; /** * @auther a123 * @create 2018-08-11 14:41 * @desc */ public class AppTest implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean { private String name; public AppTest() { System.out.println("构造方法"); } public String getName() { return name; } public void setName(String name) { this.name = name; } //自定义的初始化函数 public void myInit() { System.out.println("myInit被调用"); } //自定义的销毁方法 public void myDestroy() { System.out.println("myDestroy被调用"); } /** * .如果Bean实现了BeanFactoryAware接口,会回调该接口的setBeanFactory()方法,传入该Bean的BeanFactory,这样该Bean就获得了自己所在的BeanFactory, * @param beanFactory * @throws BeansException */ public void setBeanFactory(BeanFactory beanFactory) throws BeansException { System.out.println("调用BeanFactoryAware设置BeanFactory"); } /** * 实现BeanNameAware, 为bean设置名称 * @param s */ public void setBeanName(String s) { System.out.println("调用实现BeanNameAware 设置bean名称"); } /** * 容器关闭后,如果Bean实现了DisposableBean接口,则会回调该接口的destroy()方法, * @throws Exception */ public void destroy() throws Exception { System.out.println("容器关闭后, 调用DisposableBean的destroy()方法,会被myDestory占用"); } /** * 如果Bean实现了InitializingBean接口,则会回调该接口的afterPropertiesSet()方法, * @throws Exception */ public void afterPropertiesSet() throws Exception { System.out.println("如果Bean实现了InitializingBean接口,则会回调该接口的afterPropertiesSet()方法"); } /** * 如果Bean实现了ApplicationContextAware接口,会回调该接口的setApplicationContext()方法,传入该Bean的ApplicationContext,这样该Bean就获得了自己所在的ApplicationContext, * @param applicationContext * @throws BeansException */ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("调用ApplicationContextAware设置application"); } } package com.smart.test; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; public class MyBeanPostProcessor implements BeanPostProcessor { public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // TODO Auto-generated method stub System.out.println("postProcessBeforeInitialization被调用"); return bean; } public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // TODO Auto-generated method stub System.out.println("postProcessAfterInitialization被调用"); return bean; } } //配置文件 jack //测试方法 /** * 构造方法 * 调用实现BeanNameAware 设置bean名称 * 调用BeanFactoryAware设置BeanFactory * 调用ApplicationContextAware设置application * postProcessBeforeInitialization被调用 * 如果Bean实现了InitializingBean接口,则会回调该接口的afterPropertiesSet()方法 * myInit被调用 * postProcessAfterInitialization被调用 * xml加载完毕 * com.smart.test.AppTest@60df60da * 关闭容器 * 容器关闭后, 调用DisposableBean的destroy()方法,会被myDestory占用 * myDestroy被调用 */ @Test public void testApplicationLife(){ System.out.println("开始初始化容器"); ApplicationContext ac = new ClassPathXmlApplicationContext("apptest.xml"); System.out.println("xml加载完毕"); AppTest person1 = (AppTest) ac.getBean("apptest"); System.out.println(person1); System.out.println("关闭容器"); ((ClassPathXmlApplicationContext)ac).close(); } ``` 通过测试可知applicationContext生命周期为: 1. 创建容器 2. 调用bean构造方法,实例化bean对象,设置对象属性 3. 调用BeanNameAware的setBeanName()方法 4. 调用BeanFactoryAware的setBeanFactory()方法 5. 调用ApplicationContextAware的setApplicationContext()方法 6. 调用BeanPostProcessor的预初始化方法(before) 7. 调用InitializingBean的afterPropertiesSet()方法 8. 调用init-method 9. 调用BeanPostProcessor的后初始化方法(after) 10. 判断是否单例,单例这缓存到Spring IOC的hashMap容器中,否则将生命周期交给客户端 11. 容器关闭 12. 调用DisposableBean的afterPropertiesSet()方法 13. 调用destory-method的方法 ####ioc配置 spring提供多种配置方案,可以提供开发选择,同xml的配置, 注解,java类配置,groovy DSL配置,多种配置方案可以组合使用 xml配置样例讲解见resources下配置文件注释 注解方式有@Component @Repository 用于对DAO实现类进行注解 @Service 用与对service实现了进行注解 @Controller 用于对Controller实现类进注解, 一般用在web工程中 通过这些定义的类, 可以在xml中通过进行扫描,同时在其中也可以通过其他的配置进行过滤筛选 #### 自动装配 在容器中定义的Bean也可以通过注解的方式自动注入到变量中 @AutoWired这个注解是通过通过类型自动选择bean注入,默认情况下,如果在容器中没有对应的bean,则会抛出异常,这个时候可以通过@AutoWired(requited=false)选择在没有bean的时候不进行注入,同时也不抛出异常,如果需要通过按bean名称进行注入,可以搭配@Qualifier(""beanName"): @AutoWired(required=false) @Qualifier("beanName") private Car car; 延迟加载,通过@Lazy注解的类,不会在ApplicationContext启动的时候进行bean实例化,而是在第一次用到该bean的时候才实例化, 但是使用这个注解需要注意的是, 在类定义时候需要进行注解,在使用的时候也需要进行注解,否则将会抛异常 @Lazy @AutoWired @Resource("beanName")使用名称进行注入 BeanDefinition 负者将xml文件中的转换为容器内部表示方式 InstantiationStrategy 取出容器中的BeanDefinition,实例化bean,注意这个地方实例化出来的bean只是半成品,其属性等填充需要后续支持 BeanWrapper 完成bean的属性填充 #### 事件监听 配置文件:mail_sender_event.xml 代码 :com.smart.test.event_listener 测试方法: ``` /** * 做事件监听 */ @Test public void testEventSenderListener(){ ApplicationContext ctx = new ClassPathXmlApplicationContext("mail_sender_event.xml"); MailSender mailSender = (MailSender) ctx.getBean("mailSender"); mailSender.sendMail("aaa@bb.com"); } ``` ####国际化支持 配置文件:messages.xml messages/messages_xx.properties(不同国家的配置信息) 测试代码: ``` /** * * 简单添加国际化支持 */ @Test public void testMessages(){ ApplicationContext ctx = new ClassPathXmlApplicationContext("messages.xml"); MessageSource ms = (MessageSource)ctx.getBean("messageSource"); Object[] params = new Object[]{"mjlf", new GregorianCalendar().getTime()}; String str_ja_target = ms.getMessage("main.target", null, Locale.JAPAN); String str_ja_title = ms.getMessage("main.title", params, Locale.JAPAN); String str_en_target = ms.getMessage("main.target", null, Locale.ENGLISH); String str_en_title = ms.getMessage("main.title", params, Locale.ENGLISH); String str_cn_target = ms.getMessage("main.target", null, new Locale("cn")); String str_cn_title = ms.getMessage("main.title", params, new Locale("cn")); System.out.println("日本: " + str_ja_target); System.out.println("日本: " + str_ja_title); System.out.println("英国: " + str_en_target); System.out.println("英国: " + str_en_title); System.out.println("中国: " + str_cn_target); System.out.println("中国: " + str_cn_title); } ``` #### AOP 名称解释 连接点(JoinPoint):定义为在一个方法执行前,执行后,或者是抛出异常后的这些位置我们成为连接点,连接点只能是在方法中定义 切点(PointCut):一个程序可能存在很多连接点,如果确切的定位到我们希望的连接点,就是通过切点来完成,就好比是连接点是关系型数据库中的内容,而切点是查询条件,通过查询条件定位连接点,但是这里有一个问题,切点只能定位到连接点所在的方法,并不能定位到确定的连接点,这就需要通过位置来辅助确定,也就是执行前(before),执行后(after)等 增强(advice):增强好比是我们希望在连接点处执行与正常业务无关的事情,如果性能监控,日志打印等等 切面(aspect):增强和切点的组合,包含了横切逻辑的定义,也包含了连接点的定义 #### jdk代理 jdk代理通过Proxy类和实现InvocationHandler接口实现,而且jdk代理只能代理接口 样例:com.smart.test.proxy.jdk_proxy ``` public class PerformanceHandler implements InvocationHandler { private Object target; public PerformanceHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { PerformanceMonitor.begin(target.getClass().getName() + "." + method.getName()); Object obj = method.invoke(target, args);//通过反射调用业务类方法 PerformanceMonitor.end(); return obj; } } @Test public void testJdkProxy(){ Soming target = new DoSomeing(); PerformanceHandler handler = new PerformanceHandler(target); Soming some = (Soming)Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler); some.doEat(); some.doSleep(); } ``` #### cglib代理 cglib代理弥补了jdk代理的不足(只能代理接口) ``` public class CglibProxy implements MethodInterceptor { private Enhancer enhancer = new Enhancer(); public Object getProxy(Class clazz){ enhancer.setSuperclass(clazz); enhancer.setCallback(this); return enhancer.create(); } public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { PerformanceMonitor.begin(objects.getClass().getName() + "." + method.getName()); Object object = methodProxy.invokeSuper(o, objects); PerformanceMonitor.end(); return object; } } @Test public void testCglibProxy(){ CglibProxy proxy = new CglibProxy(); Soming soming = (DoSomeing)proxy.getProxy(DoSomeing.class); soming.doEat(); soming.doSleep(); } ``` #### 切点函数 execution() 方法匹配模式串,表示满足某一匹配模式的所有目标类方法连接点,如execution(*greetTO(..))表示所有目标类中的greetTO方法 @annotation() 方法注解类名,表示标准了特定注解的目标方法连接点,如@annotation(com.smart.anno.NeedTest)表示任何标准了@NeedTest注解的目标类方法 args() 类名, 通过判别目标类方法运行是参数对象的类型定义指定连接点,如args(com.smart.Waiter)表示所有有且仅有一个类型匹配与Waiter入参方法 @args()类型注解类名,通过判别目标类方法运行是参数对象的类是否标注特定注解类指定连接点,如@args(com.smart.Monitorable)表示任何这样的一个目标方法,它有一个入参且入参对象的类标注@Monitorable within()类名匹配串,表示的定域下的所有连接点,如within(com.smart.service.*)表示com.smart.service包中的所有连接点,即包中所有类的方法,而within(com.smart.service.*Service)表示在com.smart.service包中所有以Service结尾的类的所有连接点 target()类名, 加入目标类按类型匹配于指定类,则目标类的所有连接点匹配这个切点,如通过target(com.smart.Waiter)定义切点,Waiter即Waiter实现类NaiveWaiter中的所有连接点都匹配这个切点 @within() 类型注解类名,加入目标类按类型匹配于某个类A,且类A标注了特定注解,则目标类的所有类的所有连接点匹配这个切点,如@within(com.smart.Monitorable)定义的切点,加入Waiter类标注了@Monitorable注解,则Waiter及Waiter实现类NativeWaiter的所有连接点匹配这个切点 @target() 假如目标类标注了特定注解,则目标类的所有连接点都匹配该切点,如@target(com.smart.Monitorable),假如NativeWaiter标注了@Monitoralbe,则NativeWaiter的所有连接点都匹配这个切点 this() 类名 代理类按类型匹配于指定类,则被代理的目标类的所有连接点都匹配该切点 需要注意的是, 所有@annotation, @within, @target都是针对类而言的, 并非针对运行时的引用类型而言,所有当注解标注在接口上时,并不能拦截到其实现类 within/target @within/@target都是针对类而言的, 不同在带@的要求目标带有特定注解,within 和 target的不同在于within不包含子类, 而target可以包含本类及其子类 #### 在函数中使用到的通配符 *:匹配任意字符,但它只能匹配上下文的一个元素 ..:匹配任意字符,可以匹配上下文中的多个元素,但在表示类时,必须和*联合使用;儿在表示参数是,可以单独使用 +:表示按类型匹配指定类的所有类,必须跟在类名后面,如com.smart.Car+.继承和拓展指定类的所有类,同时还包括指定类本身 #### 逻辑运算符 &&:与操作符,相当于切点的交集, 与and相同 ||:或操作符,取并集 !:取非 #### 增强类型 @Before 前置增强 value:该成员用于定义切点 argNames: 方法参数名,多个参数使用逗号分割 @AfterReturning 后置增强 value:定义切点 pointCut:表示切点信息。如果显示指定pointcut值,那么它将覆盖value的设置值,可以将pointcut成员看做value的同义词 returning: 将目标对象方法的返回值绑定给增强的方法 argNames:方法参数名称 @Around 环绕增强 value: argNames: @AfterThrowing 抛出增强 value: pointcut: throwing: 将抛出的异常绑定到增强方法中 argNames: @After:final增强,不管是抛出异常还是正常退出,该增强都会执行 value: argNames: @DeclareParents:引介增强 value:需要注意的是, 这个地方只能匹配具体的类,不能通过配置超类或接口,然后在子类中实现 defaultImpl:默认的接口实现类 在环绕增强中可以通过以下方式获取目标方法信息: ``` /** 通过ProceedingJoinPoint获取目标方法信息 */ @Around(value = "@args(NeedTest)") public void needAnnotationArgs(ProceedingJoinPoint proceedingJoinPoint){ System.out.println("args"); System.out.println("params : " + proceedingJoinPoint.getArgs()[0]); System.out.println("class name : " + proceedingJoinPoint.getTarget().getClass().getName()); } ``` 也可以通过与args切点方法结合获取目标方法的实际参数,需要注意的是这个地方的参数要与目标方法中的方法名称保持一致,如下 ``` @Before(value = "execution(* *greet*(..)) && args(clientName, ..)") public void needExecution(String clientName){ System.out.println("execution and params : " + clientName ); } ``` #### 码云特技 1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md 2. 3. 码云官方博客 [blog.gitee.com](https://blog.gitee.com) 4. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解码云上的优秀开源项目 5. [GVP](https://gitee.com/gvp) 全称是码云最有价值开源项目,是码云综合评定出的优秀开源项目 6. 码云官方提供的使用手册 [http://git.mydoc.io/](http://git.mydoc.io/) 7. 码云封面人物是一档用来展示码云会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)