如何伪装成一个服务端开发(四)

目录

如何伪装成一个服务端开发(一)

如何伪装成一个服务端开发(二)

如何伪装成一个服务端开发(三) 

如何伪装成一个服务端开发(四)

 

Bean的生命周期

    上文我们说到了如何将一个Bean装配进入IoC容器。但是对于容器来说,这仅是其中一个步骤而已,我们有必要了解 Spring IoC 初始化和销毁 Bean 的过程,这便是 Bean 的生命周期的过程,它大致分为 Bean 定义、Bean 的初始化、Bean 的生存期和 Bean 的销毁4个部分。其中 Bean 定义过程大致如下。

  • Spring 通过我们的配置,如@ComponentScan 定义的扫描路径去找到带有@Component 的类,这个过程就是一个资源定位的过程。
  • 一旦找到了资源,那么它就开始解析,并且将定义的信息保存起来。注意,此时还没有初始化 Bean,也就没有 Bean 的实例,它有的仅仅是 Bean 的定义。
  • 然后就会把 Bean 定义发布到 Spring IoC 容器中。此时,IoC 容器也只有 Bean 的定义,还是没有 Bean 的实例生成。
     

    完成了这3步只是一个资源定位并将 Bean 的定义发布到 IoC 容器的过程,还没有 Bean 实例的生成,更没有完成依赖注入。2d8a61d0c336a364a0a553cc13a7e707e89.jpg

     在ComponentScan中有一个lazyInit的参数,该参数可以设置延迟初始化。一般情况下,我们调用

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

    之后,applicationContext这个容器就已经实例化了其中的各种Bean对象。

    我们可以

@ComponentScan(value = "com.guardz.first_spring_boot.model",lazyInit = true)

    添加了lazyInit = true之后,调用上面方法并不会创建实例,而是会在applicationContext.getBean的时候才会创建对应的Bean对象,并进行依赖注入。

                           

    上面这张图还是比较重要的,他说明了整个Bean的调用流程。我们可以介入其中的每一个步骤,比如我们修改之前的GameUser类

//重写声明周期方法通过继承相应接口实现
@Component("gameUser") 
public class GameUser implements User , BeanNameAware,
        BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean {

    private UserInfo userInfo = null;
    public GameUser(@Autowired @Qualifier("adminInfo") UserInfo userInfo){
        this.userInfo = userInfo;
        log.info("构造 GameUser");
    }

    @Override
    public void setUserInfo(UserInfo userInfo) {
        log.info("setUserInfo is called");
        this.userInfo = userInfo;
    }

    @Override
    public void printUserInfo() {
        log.info("I am Game User ,type is "+userInfo.getUserType());
    }

    @Override
    public void setBeanName(String name) {
        log.info("调用 BeanNameAware 的 setBeanName ----------");
    }


    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        log.info("调用 BeanFactoryAware 的 setBeanFactory ----------");
    }

    @Override
    public void destroy() throws Exception {
        log.info("调用 DisposableBean 的 destroy ----------");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("调用 InitializingBean 的 afterPropertiesSet ----------");
    }

    // 只有当IoC容器继承与ApplicationContext的时候才会调用这个方法
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        log.info("调用 ApplicationContextAware 的 setApplicationContext ----------");
    }

    @PostConstruct
    public void init(){
        log.info("调用 @PostConstruct 的 init ----------");
    }

    @PreDestroy
    public void customerDestroy(){
        log.info("调用@PreDestroy 的 customerDestroy ----------");
    }

}

     除了@PreDestroy 和 @PostConstruct 其他的声明周期方法都是通过 继承接口来实现的。以上方法都是对单个bean而言的,Spring 还提供了两个方法可以对所有的Bean生效。

    添加一个类

@Component
public class CustomerBeanProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws
            BeansException {
        System.out.println("CustomerBeanProcessor"
                + "postProcessBeforeInitialization方法,参数【"
                + bean.getClass().getSimpleName() + "】【" + beanName + "】 ");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        System.out.println("CustomerBeanProcessor调用"
                + "postProcessAfterInitialization方法,参数【"
                + bean.getClass().getSimpleName() + "】【" + beanName + "】 ");
        return bean;
    }
}

    该类也是通过自动注入的方式添加进入IoC容器的,另外BeanPostProcessor中用到了default关键字,他需要java8的支持。

    CustomerBeanProcessor的方法对于所有在该IoC容器中装配的Bean都会生效。

    修改XXXApplication,添加close方法关闭IoC容器,否则destroy不会被调用

       ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        User user = applicationContext.getBean(GameUser.class);
        user.printUserInfo();
        ((AnnotationConfigApplicationContext) applicationContext).close();

    以下是这段代码运行的输出

CustomerBeanProcessorpostProcessBeforeInitialization方法,参数【AdminInfo】【adminInfo】 
CustomerBeanProcessor调用postProcessAfterInitialization方法,参数【AdminInfo】【adminInfo】 
com.guardz.first_spring_boot.model.User  : 构造 GameUser
com.guardz.first_spring_boot.model.User  : 调用 BeanNameAware 的 setBeanName ----------
com.guardz.first_spring_boot.model.User  : 调用 BeanFactoryAware 的 setBeanFactory ----------
com.guardz.first_spring_boot.model.User  : 调用 ApplicationContextAware 的 setApplicationContext ----------
CustomerBeanProcessorpostProcessBeforeInitialization方法,参数【GameUser】【gameUser】 
com.guardz.first_spring_boot.model.User  : 调用 @PostConstruct 的 init ----------
com.guardz.first_spring_boot.model.User  : 调用 InitializingBean 的 afterPropertiesSet ----------
CustomerBeanProcessor调用postProcessAfterInitialization方法,参数【GameUser】【gameUser】 
//主动调用printUserInfo输出
com.guardz.first_spring_boot.model.User  : I am Game User ,type is admin
//销毁
com.guardz.first_spring_boot.model.User  : 调用@PreDestroy 的 customerDestroy ----------
com.guardz.first_spring_boot.model.User  : 调用 DisposableBean 的 destroy ----------

 

    另外,对于在AppConfig中使用@Bean注解注入的对象,我们也可以一定程度上控制初始化和销毁过程。

@Bean(name = "dataSource", initMethod = "init" ,destroyMethod = "close")

    比如上面,我们会在构造后主动调用这个bean的init方法,并且在销毁时调用close方法。对于数据库连接等bean这样使用还是非常不错的。

 

属性文件

    我们之前提到过application.properties 我们可以在其中修改很多默认配置,但是除此之外,我们还可以添加一些我们自定义的配置项,并且在项目中使用。

    为此我们需要现在pom.xml中添加依赖

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

    然后在application.properties添加属性吧,比如我添加一个jdbc的配置属性

database.driverName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/chapter3
database.username=root
database.password=123456

    

@Component
public class DataBaseProperties {
    @Value("${database.driverName}")
    private String driverName = null;

    @Value("${database.url}")
    private String url = null;
    
    @Value("${database.username}")
    public void setUsername(String username) {
        System.out.println(username);
        this.username = username;
    }

    @Value("${database.password}")
    public void setPassword(String password) {
        System.out.println(password);
        this.password = password;
    }
    ......
    /**** getters ****/    
}

    之后我们就可以在@Value注解中使用  ${..}这样的方式来读取属性文件中的值。

    另外,可以使用注解@ConfigurationProperties。

@Component
@ConfigurationProperties("database")
public class DataBaseProperties {
}

    这样就不需要在写@Value了,他会自动寻找名称匹配的值(这种情况需要名字严格匹配)。

    以上的情况我们都把属性直接写入application.properties,但是这是不方便分类管理的,我们可以将属性写在一个单独的文件中,比如 xx.properties,然后使用@PropertySource注解去选择属性文件。

    修改XXApplication文件

...
@PropertySource(value={"classpath:xx.properties"}, ignoreResourceNotFound=true)
public class XXApplication {
...
}

    value中可以配置多个值,classpath表示从类目录中寻找,比如  classpath:/com/myco/app.properties  。或者可以使用file前缀,直接定位一个属性文件,比如 file:/path/to/file.properties  .

 

条件装配

    当我们使用@Bean或者@Component时,就表示我们一定会装配这个Bean,如果Bean无法初始化,那么容器就会报出异常。这个时候条件装配就出现了

@Bean(name = "dataSource", destroyMethod = "close")
@Conditional(DatabaseConditional.class)
public DataSource getDataSource(
        ....
}

    比如如上代码,只有在满足DatabaseConditional类指定的条件的情况下,这个DataSource才会被装配。这种情况下DatabaseConditional就必须要继承与Condition接口,并且实现match方法,比如:

public class DatabaseConditional implements Condition {

    /**
     * 数据库装配条件
     *
     * @param context 条件上下文
     * @param metadata 注释类型的元数据
     * @return true 装配 Bean,否则不装配
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 取出环境配置
        Environment env = context.getEnvironment();
        // 判断属性文件是否存在对应的数据库配置
        return env.containsProperty("database.driverName") 
                && env.containsProperty("database.url")
                && env.containsProperty("database.username") 
                && env.containsProperty("database.password");
    }
}

    

Bean的作用域

    在继续理解作用域之前恐怕我们需要先了解两个在服务端开发中属于基础中的基础的概念,cookie和session。以下内容转自https://justsee.iteye.com/blog/1570652

----------------------------------------------------------------------------------------------------------------

一、术语session 

    在我的经验里,session这个词被滥用的程度大概仅次于transaction,更加有趣的是transaction与session在某些语境下的含义是相同的。 

    session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话时从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个session。有时候我们可以看到这样的话“在一个浏览器会话期间,...”,这里的会话一词用的就是其本义,是指从一个浏览器窗口打开到关闭这个期间①。最混乱的是“用户(客户端)在一次会话期间”这样一句话,它可能指用户的一系列动作(一般情况下是同某个具体目的相关的一系列动作,比如从登录到选购商品到结账登出这样一个网上购物的过程,有时候也被称为一个transaction),然而有时候也可能仅仅是指一次连接,也有可能是指含义①,其中的差别只能靠上下文来推断②。 

    然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义,“面向连接”指的是在通信双方在通信之前要先建立一个通信的渠道,比如打电话,直到对方接了电话通信才能开始,与此相对的是写信,在你把信发出去的时候你并不能确认对方的地址是否正确,通信渠道不一定能建立,但对发信人来说,通信已经开始了。“保持状态”则是指通信的一方能够把一系列的消息关联起来,使得消息之间可以互相依赖,比如一个服务员能够认出再次光临的老顾客并且记得上次这个顾客还欠店里一块钱。这一类的例子有“一个TCP session”或者“一个POP3 session”③。 

    而到了web服务器蓬勃发展的时代,session在web开发语境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器之间保持状态的解决方案④。有时候session也用来指这种解决方案的存储结构,如“把xxx保存在session里”⑤。由于各种用于web开发的语言在一定程度上都提供了对这种解决方案的支持,所以在某种特定语言的语境下,session也被用来指代该语言的解决方案,比如经常把Java里提供的javax.servlet.http.HttpSession简称为session⑥。 

    鉴于这种混乱已不可改变,本文中session一词的运用也会根据上下文有不同的含义,请大家注意分辨。 
在本文中,使用中文“浏览器会话期间”来表达含义①,使用“session机制”来表达含义④,使用“session”表达含义⑤,使用具体的“HttpSession”来表达含义⑥ 

二、HTTP协议与状态保持 

    HTTP协议本身是无状态的,这与HTTP协议本来的目的是相符的,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没有必要纪录彼此过去的行为,每一次请求之间都是独立的,好比一个顾客和一个自动售货机或者一个普通的(非会员制)大卖场之间的关系一样。 

    然而聪明(或者贪心?)的人们很快发现如果能够提供一些按需生成的动态信息会使web变得更加有用,就像给有线电视加上点播功能一样。这种需求一方面迫使HTML逐步添加了表单、脚本、DOM等客户端行为,另一方面在服务器端则出现了CGI规范以响应客户端的动态请求,作为传输载体的HTTP协议也添加了文件上载、cookie这些特性。其中cookie的作用就是为了解决HTTP协议无状态的缺陷所作出的努力。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。 

    让我们用几个例子来描述一下cookie和session机制之间的区别与联系。笔者曾经常去的一家咖啡店有喝5杯咖啡免费赠一杯咖啡的优惠,然而一次性消费5杯咖啡的机会微乎其微,这时就需要某种方式来纪录某位顾客的消费数量。想象一下其实也无外乎下面的几种方案: 

    1、该店的店员很厉害,能记住每位顾客的消费数量,只要顾客一走进咖啡店,店员就知道该怎么对待了。这种做法就是协议本身支持状态。 

    2、发给顾客一张卡片,上面记录着消费的数量,一般还有个有效期限。每次消费时,如果顾客出示这张卡片,则此次消费就会与以前或以后的消费相联系起来。这种做法就是在客户端保持状态。 

    3、发给顾客一张会员卡,除了卡号之外什么信息也不纪录,每次消费时,如果顾客出示该卡片,则店员在店里的纪录本上找到这个卡号对应的纪录添加一些消费信息。这种做法就是在服务器端保持状态。 

    由于HTTP协议是无状态的,而出于种种考虑也不希望使之成为有状态的,因此,后面两种方案就成为现实的选择。具体来说cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上它还有其他选择。 

三、理解cookie机制 

    cookie机制的基本原理就如上面的例子一样简单,但是还有几个问题需要解决:“会员卡”如何分发;“会员卡”的内容;以及客户如何使用“会员卡”。 

    正统的cookie分发是通过扩展HTTP协议来实现的,服务器通过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的cookie。然而纯粹的客户端脚本如JavaScript或者VBScript也可以生成cookie。 

    而cookie的使用是由浏览器按照一定的原则在后台自动发送给服务器的。浏览器检查所有存储的cookie,如果某个cookie所声明的作用范围大于等于将要请求的资源所在的位置,则把该cookie附在请求资源的HTTP请求头上发送给服务器。意思是麦当劳的会员卡只能在麦当劳的店里出示,如果某家分店还发行了自己的会员卡,那么进这家店的时候除了要出示麦当劳的会员卡,还要出示这家店的会员卡。 

    cookie的内容主要包括:名字,值,过期时间,路径和域。 

    其中域可以指定某一个域比如.google.com,相当于总店招牌,比如宝洁公司,也可以指定一个域下的具体某台机器比如www.google.com或者froogle.google.com,可以用飘柔来做比。 

    路径就是跟在域名后面的URL路径,比如/或者/foo等等,可以用某飘柔专柜做比。路径与域合在一起就构成了cookie的作用范围。如果不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览器会话期的cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里,当然这种行为并不是规范规定的。如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。 

    存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存里的cookie,不同的浏览器有不同的处理方式。对于IE,在一个打开的窗口上按Ctrl-N(或者从文件菜单)打开的窗口可以与原窗口共享,而使用其他方式新开的IE进程则不能共享已经打开的窗口的内存cookie;对于Mozilla Firefox0.8,所有的进程和标签页都可以共享同样的cookie。一般来说是用javascript的window.open打开的窗口会与原窗口共享内存cookie。浏览器对于会话cookie的这种只认cookie不认人的处理方式经常给采用session机制的web应用程序开发者造成很大的困扰。 

下面就是一个goolge设置cookie的响应头的例子 

    HTTP/1.1 302 Found 
    Location: http://www.google.com/intl/zh-CN/ 
    Set-Cookie: PREF=ID=0565f77e132de138:NW=1:TM=1098082649:LM=1098082649:S=KaeaCFPo49RiA_d8; 
    expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com 
    Content-Type: text/html 


四、理解session机制 

    session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。 

    当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存。 

    保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个cookie的名字都是类似于SEEESIONID,而。比如weblogic对于web应用程序生成的cookie,JSESSIONID=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764,它的名字就是JSESSIONID。 

    由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,一种是作为URL路径的附加信息,表现形式为http://...../xxx;jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764 
另一种是作为查询字符串附加在URL后面,表现形式为http://...../xxx?jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764 
这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。 
为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id。 
    在谈论session机制的时候,常常听到这样一种误解“只要关闭浏览器,session就消失了”。其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个session,否则服务器会一直保留,程序一般都是在用户做log off的时候发个指令去删除session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分session机制都使用会话cookie来保存session id,而关闭浏览器后这个session id就消失了,再次连接服务器时也就无法找到原来的session。如果服务器设置的cookie被保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的session id发送给服务器,则再次打开浏览器仍然能够找到原来的session。 

    恰恰是由于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把session删除以节省存储空间。 
五、理解javax.servlet.http.HttpSession 

    HttpSession是Java平台对session机制的实现规范,因为它仅仅是个接口,具体到每个web应用服务器的提供商,除了对规范支持之外,仍然会有一些规范里没有规定的细微差异。这里我们以BEA的Weblogic Server8.1作为例子来演示。 

    首先,Weblogic Server提供了一系列的参数来控制它的HttpSession的实现,包括使用cookie的开关选项,使用URL重写的开关选项,session持久化的设置,session失效时间的设置,以及针对cookie的各种设置,比如设置cookie的名字、路径、域,cookie的生存时间等。 

    一般情况下,session都是存储在内存里,当服务器进程被停止或者重启的时候,内存里的session也会被清空,如果设置了session的持久化特性,服务器就会把session保存到硬盘上,当服务器进程重新启动或这些信息将能够被再次使用,Weblogic Server支持的持久性方式包括文件、数据库、客户端cookie保存和复制。 

    复制严格说来不算持久化保存,因为session实际上还是保存在内存里,不过同样的信息被复制到各个cluster内的服务器进程中,这样即使某个服务器进程停止工作也仍然可以从其他进程中取得session。 

    cookie生存时间的设置则会影响浏览器生成的cookie是否是一个会话cookie。默认是使用会话cookie。有兴趣的可以用它来试验我们在第四节里提到的那个误解。 

    cookie的路径对于web应用程序来说是一个非常重要的选项,Weblogic Server对这个选项的默认处理方式使得它与其他服务器有明显的区别。后面我们会专题讨论。 

    关于session的设置参考[5] http://e-docs.bea.com/wls/docs70/webapp/weblogic_xml.html#1036869 

六、HttpSession常见问题(在本小节中session的含义为⑤和⑥的混合) 

    1、session在何时被创建 

    一个常见的误解是以为session在有客户端访问时就被创建,然而事实是直到某server端程序调用HttpServletRequest.getSession(true)这样的语句时才被创建,注意如果JSP没有显示的使用 <%@page session="false"%> 关闭session,则JSP文件在编译成Servlet时将会自动加上这样一条语句HttpSession session = HttpServletRequest.getSession(true);这也是JSP中隐含的session对象的来历。 

    由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它。 

    2、session何时被删除 

    综合前面的讨论,session在下列情况下被删除a.程序调用HttpSession.invalidate();或b.距离上一次收到客户端发送的session id时间间隔超过了session的超时设置;或c.服务器进程被停止(非持久session) 

    3、如何做到在浏览器关闭时删除session 

    严格的讲,做不到这一点。可以做一点努力的办法是在所有的客户端页面里使用javascript代码window.oncolose来监视浏览器的关闭动作,然后向服务器发送一个请求来删除session。但是对于浏览器崩溃或者强行杀死进程这些非常规手段仍然无能为力。 

    4、有个HttpSessionListener是怎么回事 

    你可以创建这样的listener去监控session的创建和销毁事件,使得在发生这样的事件时你可以做一些相应的工作。注意是session的创建和销毁动作触发listener,而不是相反。类似的与HttpSession有关的listener还有HttpSessionBindingListener,HttpSessionActivationListener和HttpSessionAttributeListener。 

    5、存放在session中的对象必须是可序列化的吗 

    不是必需的。要求对象可序列化只是为了session能够在集群中被复制或者能够持久保存或者在必要时server能够暂时把session交换出内存。在Weblogic Server的session中放置一个不可序列化的对象在控制台上会收到一个警告。我所用过的某个iPlanet版本如果session中有不可序列化的对象,在session销毁时会有一个Exception,很奇怪。 

    6、如何才能正确的应付客户端禁止cookie的可能性 

    对所有的URL使用URL重写,包括超链接,form的action,和重定向的URL,具体做法参见[6] 
http://e-docs.bea.com/wls/docs70/webapp/sessions.html#100770 

    7、开两个浏览器窗口访问应用程序会使用同一个session还是不同的session 

    参见第三小节对cookie的讨论,对session来说是只认id不认人,因此不同的浏览器,不同的窗口打开方式以及不同的cookie存储方式都会对这个问题的答案有影响。 

    8、如何防止用户打开两个浏览器窗口操作导致的session混乱 

    这个问题与防止表单多次提交是类似的,可以通过设置客户端的令牌来解决。就是在服务器每次生成一个不同的id返回给客户端,同时保存在session里,客户端提交表单时必须把这个id也返回服务器,程序首先比较返回的id与保存在session里的值是否一致,如果不一致则说明本次操作已经被提交过了。可以参看《J2EE核心模式》关于表示层模式的部分。需要注意的是对于使用javascript window.open打开的窗口,一般不设置这个id,或者使用单独的id,以防主窗口无法操作,建议不要再window.open打开的窗口里做修改操作,这样就可以不用设置。 

    9、为什么在Weblogic Server中改变session的值后要重新调用一次session.setValue 
    
    做这个动作主要是为了在集群环境中提示Weblogic Server session中的值发生了改变,需要向其他服务器进程复制新的session值。 

    10、为什么session不见了 

    排除session正常失效的因素之外,服务器本身的可能性应该是微乎其微的,虽然笔者在iPlanet6SP1加若干补丁的Solaris版本上倒也遇到过;浏览器插件的可能性次之,笔者也遇到过3721插件造成的问题;理论上防火墙或者代理服务器在cookie处理上也有可能会出现问题。 
出现这一问题的大部分原因都是程序的错误,最常见的就是在一个应用程序中去访问另外一个应用程序。我们在下一节讨论这个问题。 

    七、跨应用程序的session共享 

    常常有这样的情况,一个大项目被分割成若干小项目开发,为了能够互不干扰,要求每个小项目作为一个单独的web应用程序开发,可是到了最后突然发现某几个小项目之间需要共享一些信息,或者想使用session来实现SSO(single sign on),在session中保存login的用户信息,最自然的要求是应用程序间能够访问彼此的session。 

    然而按照Servlet规范,session的作用范围应该仅仅限于当前应用程序下,不同的应用程序之间是不能够互相访问对方的session的。各个应用服务器从实际效果上都遵守了这一规范,但是实现的细节却可能各有不同,因此解决跨应用程序session共享的方法也各不相同。 

----------------------------------------------------------------------------------------------------------------

    以上,内容让笔者觉得自己离服务端开发又接近了一步。现在我们回过神来看下IoC容器中Bean的作用域。

作用域类型使用范围作用域描述
singleton所有 Spring 应用默认值,IoC 容器只存在单例
prototype所有 Spring 应用每当从 IoC 容器中取出一个 Bean,则创建一个新的 Bean
sessionSpring Web 应用HTTP 会话
applicationSpring Web 应用Web 工程生命周期
requestSpring Web 应用Web 工程单次请求(request)
globalSessionSpring Web 应用在一个全局的 HTTP Session 中,一个 Bean 定义对应一个实例。实践中基本不使用

    application作用域一般可以使用singleton来替代,而globalSession一般不会使用。

    我们先来看下我们是如何实现作用域控制的。

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ScopeBean {
}

    通过@Scope我们可以设置作用域,比如上面设置成了prototype 模式,我们可以使  @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) 设置成单例,但是默认就是单例,所以还不如不设置了吧。

    这里的 ConfigurableBeanFactory 只能提供单例(SCOPE_SINGLETON)和原型(SCOPE_PROTOTYPE)两种作用域供选择,如果是在 Spring MVC 环境中,还可以使用 WebApplicationContext 去定义其他作用域,如请求(SCOPE_REQUEST)、会话(SCOPE_SESSION)和应用(SCOPE_APPLICATION)。例如,下面的代码就是定义请求作用域。

package com.springboot.chapter3.scope.pojo;
/******** imports ********/
@Component
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class ScopeBean {
}

这样同一个请求范围内去获取这个 Bean 的时候,只会共用同一个 Bean,第二次请求就会产生新的 Bean。

@Profile

    在实际开发中我们会有很多环境,比如线上环境,比如开发环境等……有时候在不同的环境下我们的对象可能有些不一样,最直接的比如数据库,线上和开发环境使用不同的数据库,所以数据库连接注入的时候也会有所区别。

    我们可以使用@Profile注解来区分环境注入。

@Bean(name = "dataSource", destroyMethod = "close")
@Profile("dev")
public DataSource getDevDataSource() {
    ........
}

@Bean(name = "dataSource", destroyMethod = "close")
@Profile("test")
public DataSource getTestDataSource() {
    ........
}

    比如上面这样在dev环境,注入上面的bean,在test环境注入下面的bean。

    PS: @Profile里面的内容是个数组,可以设置过个环境。

    PS2: 我们也可以直接在@Component注解的Bean下面使用@Profile

    在 Spring 中存在两个参数可以提供给我们配置,以修改启动 Profile 机制,一个是 spring.profiles.active,另一个是 spring.profiles.default。在这两个属性都没有配置的情况下,Spring 将不会启动 Profile 机制,这就意味着被@Profile 标注的 Bean 将不会被 Spring 装配到 IoC 容器中。Spring 是先判定是否存在 spring.profiles.active 配置后,再去查找 spring.profiles.default 配置的,所以 spring.profiles.active 的优先级要大于 spring.profiles.default。

在 Java 启动项目中,我们只需要如下配置就能够启动 Profile 机制:

                                    JAVA_OPTS="-Dspring.profiles.active=dev"

    在idea中可以在如下位置设置

                                        9b63658c0180d0dac73d200ff5c98b844c5.jpg

    另外我们可以创建一个application-dev.properties文件配置文件,当我们使用dev环境的时候,spring会自动选用application-dev.properties配置文件。

使用Spring EL

在上述代码中,我们是在没有任何运算规则的情况下装配 Bean 的。为了更加灵活,Spring 还提供了表达式语言 Spring EL。通过 Spring EL 可以拥有更为强大的运算规则来更好地装配 Bean。

最常用的当然是读取属性文件的值,例如:

@Value("${database.driverName}") 
String driver

这里的@Value 中的${......}代表占位符,它会读取上下文的属性值装配到属性中,这便是一个最简单的 Spring 表达式。除此之外,它还能够调用方法,例如,我们记录一个 Bean 的初始化时间:

@Value("#{T(System).currentTimeMillis()}")
private Long initTime = null;

注意,这里采用#{......}代表启用 Spring 表达式,它将具有运算的功能;T(.....)代表的是引入类;System 是 java.lang.*包的类,这是 Java 默认加载的包,因此可以不必写全限定名,如果是其他的包,则需要写出全限定名才能引用类;currentTimeMillis 是它的静态(static)方法,也就是我们调用一次 System.currentTimeMillis()方法来为这个属性赋值。

此外还可以给属性直接进行赋值

// 赋值字符串
@Value("#{'使用 Spring EL 赋值字符串'}")
private String str = null;

// 科学计数法赋值
@Value("#{9.3E3}")
private double d;

// 赋值浮点数
@Value("#{3.14}")
private float pi;

显然这比较灵活,有时候我们还可以获取其他 Spring Bean 的属性来给当前的 Bean 属性赋值,例如:

@Value("#{beanName.str}")
private String otherBeanProp = null;

注意,这里的 beanName 是 Spring IoC 容器 Bean 的名称。str 是其属性,代表引用对应的 Bean 的属性给当前属性赋值。有时候,我们还希望这个属性的字母全部变为大写,这个时候就可以写成:

@Value("#{beanName.str?.toUpperCase()}")
private String otherBeanProp = null;

再次注意这里的 Spring EL。这里引用 str 属性后跟着是一个?,这个符号的含义是判断这个属性是否为空。如果不为空才会去执行 toUpperCase 方法,进而把引用到的属性转换为大写,赋予当前属性。除此之外,还可以使用 Spring EL 进行一定的运算,如

#数学运算
@Value("#{1+2}")
private int run;

#浮点数比较运算
@Value("#{beanName.pi == 3.14f}")
private boolean piFlag;

#字符串比较运算
@Value("#{beanName.str eq 'Spring Boot'}")
private boolean strFlag;

#字符串连接
@Value("#{beanName.str + '  连接字符串'}")
private String strApp = null;

#三元运算
@Value("#{beanName.d > 1000 ? '大于' : '小于'}")
private String resultDesc = null;

从上面的代码可以看出,Spring EL 能够支持的运算还有很多,其中等值比较如果是数字型的可以使用==比较符,如果是字符串型的可以使用 eq 比较符。

引入 XML 配置 Bean

尽管 Spring Boot 建议使用注解和扫描配置 Bean,但是同样地,它并不拒绝使用 XML 配置 Bean,换句话说,我们也可以在 Spring Boot 中使用 XML 对 Bean 进行配置。这里需要使用的是注解@ImportResource,通过它可以引入对应的 XML 文件,用以加载 Bean。有时候有些框架(如 Dubbo)是基于 Spring 的 XML 方式进行开发的,这个时候需要引入 XML 的方式来实现配置。假设存在Bean


public class Squirrel implements Animal {
    @Override
    public void use() {
        System.out.println("松鼠可以采集松果");
    }
}

注意,这个 POJO 所在的包并不在@ComponentScan 定义的扫描包 com.springboot.chapter3.*之内,而且没有标注@Component,所以不会被扫描机制所装配。在这里,我们使用 XML 的方式来装配它,于是就可以定义一个 XML 文件

<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.xsd">
       <bean id="squirrel" class="com.xx.xx.xx.Squirrel"/>
</beans>

这样我们就定义了一个 Bean,然后在 Java 配置文件中就可以直接载入它

package com.springboot.chapter3.config;
/******* imports ********/
@Configuration
@ComponentScan(basePackages = "com.springboot.chapter3.*")
@ImportResource(value = {"classpath:spring-other.xml"})
public class AppConfig {
......
}

这样就可以引入对应的 XML,从而将 XML 定义的 Bean 也装配到 IoC 容器中

 

参考&摘录

 《深入浅出 Spring Boot 2.x》

  https://justsee.iteye.com/blog/1570652    

转载于:https://my.oschina.net/zzxzzg/blog/2994114

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值