Spring Boot其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。
1. 创建独立的Spring应用程序
2. 嵌入的Tomcat,无需部署WAR文件
3. 简化Maven配置
4. 自动配置Spring
5. 提供生产就绪型功能,如指标,健康检查和外部配置
6.开箱即用,没有代码生成,也无需XML配置。
l 为基于Spring的开发提供更快的入门体验
l 开箱即用,没有代码生成,也无需XML配置。同时也可以修改默认值来满足特定的需求。
l 提供了一些大型项目中常见的非功能特性,如嵌入式服务器、安全、指标,健康检测、外部配置等。
l Spring Boot并不是对Spring功能上的增强,而是提供了一种快速使用Spring的方式。
<projectxmlns=" POM/4.0.0"xmlns:xsi=" /2001/XMLSchema-instance" xsi:schemaLocation= /POM/4.0.0 /xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.kfit</groupId> <artifactId>spring-boot-hello</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>spring-boot-hello</name> <url> apache.org</url> <!-- spring boot 父节点依赖,引入这个之后相关的引入就不需要添加version配置,spring boot会自动选择最合适的版本进行添加。 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.1.RELEASE</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 指定一下jdk的版本 ,这里我们使用jdk 1.8 ,默认是1.6 --> <java.version>1.8</java.version> </properties> <dependencies> <!-- spring-boot-starter-web: MVC,AOP的依赖包.... --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!-- <version></version> 由于我们在上面指定了 parent(spring boot) --> </dependency> <dependencies> </project> |
l Codeing 步骤:
l 新建一个Controller类
package com.kfit; import java.util.Date; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * 在这里我们使用RestController (等待于 @Controller 和 @RequestBody) */ @RestController public class HelloController { /** * 在这里我们使用@RequestMapping 建立请求映射: * http://127.0.0.1:8080/hello * @return */ @RequestMapping("/hello") public String hello(){ return "hello-2016-12-11.v.0"; } @RequestMapping("/hello2") public String hello2(){ return "hello2-2016"; } @RequestMapping("/hello3") public String hello3(){ return "hello3"; } /** * Spring Boot默认使用的json解析框架是jackson * @return */ @RequestMapping("/getDemo") public Demo getDemo(){ Demo demo=new Demo(); demo.setId(1); demo.setName("张三"); demo.setCreateTime(new Date()); demo.setRemarks("这是备注信息"); return demo; } } |
l 新建启动类(App – Main方法)
package com.kfit; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.context.annotation.Bean; import org.springframework.http.converter.HttpMessageConverter; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; /** * 在这里我们使用@SpringBootApplication指定这是一个 spring boot的应用程序. */ //extends WebMvcConfigurerAdapter @SpringBootApplication public class App{ /** * 这是springloader的配置方式:-javaagent:.\lib\springloaded-1.2.4.RELEASE.jar -noverify * @param args */ public static void main(String[] args) { /* * 在main方法进行启动我们的应用程序. */ SpringApplication.run(App.class, args); } } |
l 测试代码
l 步骤:
l 1. 编写实体类Demo
package com.kfit; import java.util.Date; import com.alibaba.fastjson.annotation.JSONField; /** * 这是一个测试实体类. */ public class Demo { private int id; private String name; //com.alibaba.fastjson.annotation.JSONField @JSONField(format="yyyy-MM-dd HH:mm") private Date createTime;//创建时间. /* * 我们不想返回remarks? * serialize:是否需要序列化属性. */ @JSONField(serialize=false) private String remarks;//备注信息. public String getRemarks() { return remarks; } public void setRemarks(String remarks) { this.remarks=remarks; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime=createTime; } public int getId() { return id; } public void setId(int id) { this.id=id; } public String getName() { return name; } public void setName(String name) { this.name=name; } } |
l 2. 编写getDemo()方法
/** * Spring Boot默认使用的json解析框架是jackson * @return */ @RequestMapping("/getDemo") public Demo getDemo(){ Demo demo=new Demo(); demo.setId(1); demo.setName("张三"); demo.setCreateTime(new Date()); demo.setRemarks("这是备注信息"); returndemo; } |
l 3. 测试
l 个人使用比较习惯的json框架是fastjson,所以spring boot默认的json使用起来比较不习惯,所以很自然我就想我能不能使用fastjson进行json解析呢?
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.15</version>
</dependency>
这里要说下很重要的话,官方文档说的1.2.10以后,会有两个方法支持HttpMessageconvert,一个是FastJsonHttpMessageConverter,支持4.2以下的版本,一个是FastJsonHttpMessageConverter4支持4.2以上的版本,具体有什么区别暂时没有深入研究。这里也就是说:低版本的就不支持了,所以这里最低要求就是1.2.10+。
l 第一种方法就是:
l (1)启动类继承extends WebMvcConfigurerAdapter
l (2)覆盖方法configureMessageConverters
package com.kfit; import java.util.List; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; /** * 在这里我们使用@SpringBootApplication指定这是一个 spring boot的应用程序. */ //extends WebMvcConfigurerAdapter @SpringBootApplication publicclass App extends WebMvcConfigurerAdapter{ @Override publicvoid configureMessageConverters(List<HttpMessageConverter<?>>converters) { super.configureMessageConverters(converters); // 1、需要先定义一个 convert 转换消息的对象; FastJsonHttpMessageConverter fastConverter=new FastJsonHttpMessageConverter(); //2、添加fastJson 的配置信息,比如:是否要格式化返回的json数据; FastJsonConfig fastJsonConfig=new FastJsonConfig(); fastJsonConfig.setSerializerFeatures( SerializerFeature.PrettyFormat ); //3、在convert中添加配置信息. fastConverter.setFastJsonConfig(fastJsonConfig); //4、将convert添加到converters当中. converters.add(fastConverter); } /** * 这是springloader的配置方式:-javaagent:.\lib\springloaded-1.2.4.RELEASE.jar -noverify * @param args */ publicstaticvoid main(String[] args) { /* * 在main方法进行启动我们的应用程序. */ SpringApplication.run(App.class, args); } } |
第二种方法
l (1)在App.java启动类中,注入Bean : HttpMessageConverters
package com.kfit; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.context.annotation.Bean; import org.springframework.http.converter.HttpMessageConverter; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; /** * 在这里我们使用@SpringBootApplication指定这是一个 spring boot的应用程序. */ @SpringBootApplication public class App{ /** * 在这里我们使用 @Bean注入 fastJsonHttpMessageConvert * @return */ @Bean public HttpMessageConverters fastJsonHttpMessageConverters() { // 1、需要先定义一个 convert 转换消息的对象; FastJsonHttpMessageConverter fastConverter=new FastJsonHttpMessageConverter(); //2、添加fastJson 的配置信息,比如:是否要格式化返回的json数据; FastJsonConfig fastJsonConfig=new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); //3、在convert中添加配置信息. fastConverter.setFastJsonConfig(fastJsonConfig); HttpMessageConverter<?> converter=fastConverter; return new HttpMessageConverters(converter); } /** * 这是springloader的配置方式:-javaagent:.\lib\springloaded-1.2.4.RELEASE.jar -noverify * @param args */ public static void main(String[] args) { /* * 在main方法进行启动我们的应用程序. */ SpringApplication.run(App.class, args); } } |
l 问题的提出:
l 在编写代码的时候,你会发现我们只是简单把打印信息改变了,就需要重新部署,如果是这样的编码方式,那么我们估计一天下来就真的是打几个Hello World就下班了。那么如何解决热部署的问题呢?那就是springloaded
l 在pom.xml文件添加依赖包:
<!-- 在这里添加springloaderplugin--> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> <version>1.2.4.RELEASE</version> </dependency> </dependencies> <executions> <execution> <goals> <goal>repackage</goal> </goals> <configuration> <classifier>exec</classifier> </configuration> </execution> </executions> </plugin> |
运行方法一:使用spring-boot:run
运行方法二:
l 如果使用的run as – java application的话,那么还需要做一些处理。
l 把spring-loader-1.2.4.RELEASE.jar下载下来,放到项目的lib目录中,然后把IDEA的run参数里VM参数设置为:
l -javaagent:.\lib\springloaded-1.2.4.RELEASE.jar -noverify
l 然后启动就可以了,这样在run as的时候,也能进行热部署
l 问题的提出:
l 通过使用springloaded进行热部署,但是些代码修改了,并不会进行热部署。
l spring-boot-devtools 是一个为开发者服务的一个模块,其中最重要的功能就是自动应用代码更改到最新的App上面去。原理是在发现代码有更改之后,重新启动应用,但是速度比手动停止后再启动还要更快,更快指的不是节省出来的手工操作的时间。
l 其深层原理是使用了两个ClassLoader,一个Classloader加载那些不会改变的类(第三方Jar包),另一个ClassLoader加载会更改的类,称为 restart ClassLoader
l ,这样在有代码更改的时候,原来的restart ClassLoader 被丢弃,重新创建一个restart ClassLoader,由于需要加载的类相比较少,所以实现了较快的重启时间(5秒以内)。
l 添加依赖包:
<!-- spring boot devtools 依赖包. --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <scope>true</scope> </dependency> <!-- 这是spring boot devtoolplugin --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <!--fork : 如果没有该项配置,肯呢个devtools不会起作用,即应用不会restart --> <fork>true</fork> </configuration> </plugin> |
说明
l 1. devtools会监听classpath下的文件变动,并且会立即重启应用(发生在保存时机),注意:因为其采用的虚拟机机制,该项重启是很快的。
l 2. devtools可以实现页面热部署(即页面修改后会立即生效,这个可以直接在application.properties文件中配置spring.thymeleaf.cache=false来实现(这里注意不同的模板配置不一样)。
l 修改类-->保存:应用会重启
l 修改配置文件-->保存:应用会重启
l 修改页面-->保存:应用会重启,页面会刷新(原理是将spring.thymeleaf.cache设为false)
l 对应的spring-boot版本是否正确,这里使用的是1.4.1版本;
l 是否加入plugin以及属性<fork>true</fork>
l Eclipse Project 是否开启了Build Automatically(我自己就在这里栽了坑,不知道为什么我的工具什么时候关闭了自动编译的功能)。
l 如果设置SpringApplication.setRegisterShutdownHook(false),则自动重启将不起作用。
什么是JPA?
l JPA全称Java Persistence API.JPA通过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。百度百科JPA
什么是JPA?
l 在上面只是一个JPA的定义,我们看看另外一段更能看出是什么的描述:
l JPA(Java Persistence API)是Sun官方提出的Java持久化规范。它为Java开发人员提供了一种对象/关系映射工具来管理Java应用中的关系数据。
什么是JPA?
l 持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的对象存储在的数据库中,或者存储在磁盘文件中、XML数据文件中等等。
l 持久化是将程序数据在持久状态和瞬时状态间转换的机制。
l JDBC就是一种持久化机制。文件IO也是一种持久化机制。
什么是JPA?
l “规范”: 所谓的规范意指明文规定或约定俗成的标准。如:道德规范、技术规范,公司管理规范。
l 那么“持久化规范”就是Sun针对持久化这一层操作指定的规范,如果没有指定JPA规范,那么新起的框架就随意按照自己的标准来了,那我们开发人员就没法把我们的经历全部集中在我们的业务层上,而是在想如何进行兼容,这种情况有点像Android开发,Android本身有官方的SDK,但是由于SDK过于开源了,结果导致很多厂商基于SDK二次开发,但是兼容性就不是很好,最好的例子就是Android的头像上传,就是一件很烦人的事情。好了,JPA就介绍到这里。
什么是Hibernate?
l 这里引用百度百科的话hibernate:
l Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 Hibernate可以应用在任何使用JDBC的场合,既可以在Java的客户端程序使用,也可以在Servlet/JSP的Web应用中使用,最具革命意义的是,Hibernate可以在应用EJB的J2EE架构中取代CMP,完成数据持久化的重任。
什么是Hibernate?
l 那么是ORM呢? ORM是对象关系映射的意思,英语:Object Relational Mapping简称ORM,是一种程序技术,用于实现面向对象编程语言里不同系统类型的系统之间的数据转换。好了,更多的概念需要自己去挖掘,这里只是抛装引玉下。
什么是Spring Data?
l Spring Data是一个用于简化数据库访问,并支持云服务的开源框架。其主要目标是使得数据库的访问变得方便快捷,并支持map-reduce框架和云计算数据服务。此外,它还支持基于关系型数据库的数据服务,如Oracle RAC等。对于拥有海量数据的项目,可以用Spring Data来简化项目的开发,就如Spring Framework对JDBC、ORM的支持一样,Spring Data会让数据的访问变得更加方便。
什么是Spring Data JPA?
l 我们先看一个描述:
l Spring Data JPA能干什么
l 可以极大的简化JPA的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了CRUD外,还包括如分页、排序等一些常用的功能。
l 首先我们需要清楚的是Spring Data是一个开源框架,在这个框架中Spring Data JPA只是这个框架中的一个模块,所以名称才叫Spring Data JPA。如果单独使用JPA开发,你会发现这个代码量和使用JDBC开发一样有点烦人,所以Spring Data JPA的出现就是为了简化JPA的写法,让你只需要编写一个接口继承一个类就能实现CRUD操作了。
JPA/Hibernate 关系?
l 我们先看下别人的描述:
l JPA是一种规范,而Hibernate是它的一种实现。除了Hibernate,还有EclipseLink(曾经的toplink),OpenJPA等可供选择,所以使用Jpa的一个好处是,可以更换实现而不必改动太多代码。
l
<!-- 添加MySQL数据库驱动依赖包. --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 添加Spring-data-jpa依赖. --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> |
######################################################## ###datasource -- \u6307\u5b9amysql\u6570\u636e\u5e93\u8fde\u63a5\u4fe1\u606f. ######################################################## spring.datasource.url=jdbc:mysql: spring.datasource.username= spring.datasource.password= spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.max-active=20 spring.datasource.max-idle=8 spring.datasource.min-idle=8 spring.datasource.initial-size=10 ######################################################## ### Java Persistence Api -- Spring jpa\u7684\u914d\u7f6e\u4fe1\u606f. ######################################################## # Specify the DBMS spring.jpa.database=MYSQL # Show or not log for each sql query spring.jpa.show-sql=true # Hibernateddl auto (create, create-drop, update) spring.jpa.hibernate.ddl-auto=update # Naming strategy #[org.hibernate.cfg.ImprovedNamingStrategy #org.hibernate.cfg.DefaultNamingStrategy] spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy # stripped before adding them to the entity manager) spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect |
(1) 创建实体类Demo,如果已经存在,可以忽略。
package com.kfit.demo.bean; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; /** * 创建了一个实体类。 * 如何持久化呢? * 1、使用@Entity进行实体类的持久化操作,当JPA检测到我们的实体类当中有 * @Entity 注解的时候,会在数据库中生成对应的表结构信息。 * 如何指定主键以及主键的生成策略? * 2、使用@Id指定主键. */ @Entity public class Cat { /** * 使用@Id指定主键. * 使用代码@GeneratedValue(strategy=GenerationType.AUTO) * 指定主键的生成策略,mysql默认的是自增长。 * */ @Id @GeneratedValue(strategy=GenerationType.AUTO) private int id;//主键. private String catName;//. cat_name private int catAge;// cat_age; public int getId() { return id; } public void setId(int id) { this.id=id; } public String getCatName() { return catName; } public void setCatName(String catName) { this.catName=catName; } public int getCatAge() { return catAge; } public void setCatAge(int catAge) { this.catAge=catAge; } } |
(2) 创建jpa repository类操作持久化(CrudRepository)。
package com.kfit.demo.repository; import org.springframework.data.repository.CrudRepository; import com.kfit.demo.bean.Cat; public interface CatRepository extends CrudRepository<Cat, Integer>{ } |
(3) 创建service类。
package com.kfit.demo.service; import javax.annotation.Resource; import javax.transaction.Transactional; import org.springframework.stereotype.Service; import com.kfit.demo.bean.Cat; import com.kfit.demo.repository.CatRepository; @Service public class CatService { @Resource private CatRepository catRepository; /** * save,update ,delete 方法需要绑定事务. * 使用@Transactional进行事务的绑定. * @param cat */ //保存数据. @Transactional public void save(Cat cat){ catRepository.save(cat); } //删除数据》 @Transactional public void delete(int id){ catRepository.delete(id); } //查询数据. public Iterable<Cat> getAll(){ return catRepository.findAll(); } } |
(4) 创建restful请求类。
package com.kfit.demo.controller; import javax.annotation.Resource; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.kfit.demo.bean.Cat; import com.kfit.demo.service.CatService; @RestController @RequestMapping("/cat") public class CatController { @Resource private CatService catService; @RequestMapping("/save") public String save(){ Cat cat=new Cat(); cat.setCatName("jack"); cat.setCatAge(3); catService.save(cat); return "save ok."; } @RequestMapping("/delete") public String delete(){ catService.delete(1); return "delete ok"; } @RequestMapping("/getAll") public Iterable<Cat> getAll(){ return catService.getAll(); } } |
(5) 测试;
Repository 接口是 Spring Data 的一个核心接口,它不提供任何方法,开发者需要在自己定义的接口中声明需要的方法 :
public interface Repository<T, ID extends Serializable> { }
有这么几点需要强调下:
1. Repository是一个空接口,即是一个标记接口;
2. 若我们定义的接口继承了Repository,则该接口会被IOC容器识别为一个Repository Bean纳入到IOC容器中,进而可以在该接口中定义满足一定规范的方法。
3. 实际上也可以通过@RepositoryDefinition,注解来替代继承Repository接口。
4. 查询方法以find | read | get开头;
5. 涉及查询条件时,条件的属性用条件关键字连接,要注意的是条件属性以首字母大写。
6.使用@Query注解可以自定义JPQL语句实现更灵活的查询。
CrudRepository 接口提供了最基本的对实体类的添删改查操作
--T save(T entity);//保存单个实体
--Iterable<T> save(Iterable<? extends T> entities);//保存集合
--T findOne(ID id);//根据id查找实体
--boolean exists(ID id);//根据id判断实体是否存在
--Iterable<T> findAll();//查询所有实体,不用或慎用!
--long count();//查询实体数量
--void delete(ID id);//根据Id删除实体
--void delete(T entity);//删除一个实体
--void delete(Iterable<? extends T> entities);//删除一个实体的集合
--void deleteAll();//删除所有实体,不用或慎用!
该接口提供了分页与排序功能
--Iterable<T> findAll(Sort sort); //排序
--Page<T> findAll(Pageable pageable); //分页查询(含排序功能)
package com.kfit.demo.repository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import com.kfit.demo.bean.Cat; public interface Cat2Repository extends PagingAndSortingRepository<Cat, Integer>{ /** * 1/ 查询方法 以 get | find | read 开头. * 2/ 涉及查询条件时,条件的属性用条件关键字连接,要注意的是条件属性以首字母大写。 */ //根据catName进行查询 : 根据catName进行查询. public Cat findByCatName(String catName); /** * 如何编写JPQL语句, * Hibernate -- HQL语句. * JPQL 语句 和HQL语句是类似的. */ @Query("from Cat where catName=:cn") public Cat findMyCatName(@Param("cn")String catName); } |
JpaRepository:查找所有实体,排序、查找所有实体,执行缓存与数据库同步
JpaSpecificationExecutor:不属于Repository体系,实现一组 JPA Criteria 查询相关的方法,封装 JPA Criteria 查询条件。通常使用匿名内部类的方式来创建该接口的对象。
自定义 Repository:可以自己定义一个MyRepository接口。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
如果在JPA已经加入的话,则可以不用引入以上的配置。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
那么只需要在需要使用的类中加入:
@Resource
private JdbcTemplate jdbcTemplate;
声明为:@Repository,引入JdbcTemplate
public Demo getById(long id){
String sql="select *from Demo where id=?";
RowMapper<Demo> rowMapper=new BeanPropertyRowMapper<Demo>(Demo.class);
return jdbcTemplate.queryForObject(sql, rowMapper,id);
}
package com.kfit.demo.dao; import javax.annotation.Resource; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; import com.kfit.demo.bean.Cat; /** * 使用@Repository注解,标注这是一个持久化操作对象. */ @Repository publicclass CatDao { @Resource private JdbcTemplate jdbcTemplate; public Cat selectByCatName(String catName){ /** * 1、定义一个Sql语句; * 2、定义一个RowMapper. * 3、执行查询方法. */ String sql="select * from cat where cat_name=?"; RowMapper<Cat>rowMapper=new BeanPropertyRowMapper<>(Cat.class); Cat cat=jdbcTemplate.queryForObject(sql, new Object[]{catName}, rowMapper); returncat; } } |
@Resource
private DemoDao demoDao;
public void save(Demo demo){
demoDao.save(demo);
}
package com.kfit.demo.service; import javax.annotation.Resource; import javax.transaction.Transactional; import org.springframework.stereotype.Service; import com.kfit.demo.bean.Cat; import com.kfit.demo.dao.CatDao; import com.kfit.demo.repository.Cat2Repository; import com.kfit.demo.repository.CatRepository; @Service public class CatService { @Resource private CatRepository catRepository; @Resource private Cat2Repository cat2Repository; @Resource private CatDao catDao; /** * save,update ,delete 方法需要绑定事务. * 使用@Transactional进行事务的绑定. * @param cat */ //保存数据. @Transactional public void save(Cat cat){ catRepository.save(cat); } //删除数据》 @Transactional public void delete(int id){ catRepository.delete(id); } //查询数据. public Iterable<Cat> getAll(){ return catRepository.findAll(); } public Cat findByCatName(String catName){ return cat2Repository.findByCatName(catName); } public Cat findByCatName2(String catName){ return cat2Repository.findMyCatName(catName); } public Cat selectByCatName(String catName){ return catDao.selectByCatName(catName); } } |
@Resource
private DemoService demoService;
@RequestMapping("/getById")
public Demo getById(long id){
return demoService.getById(id);
}
package com.kfit.demo.controller; import javax.annotation.Resource; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.kfit.demo.bean.Cat; import com.kfit.demo.service.CatService; @RestController @RequestMapping("/cat") public class CatController { @Resource private CatService catService; @RequestMapping("/save") public String save(){ Cat cat=new Cat(); cat.setCatName("jack"); cat.setCatAge(3); catService.save(cat); return "save ok."; } @RequestMapping("/delete") public String delete(){ catService.delete(1); return "delete ok"; } @RequestMapping("/getAll") public Iterable<Cat> getAll(){ return catService.getAll(); } @RequestMapping("/findByCatName") public Cat findByCatName(String catName){ return catService.findByCatName(catName); } @RequestMapping("/findByCatName2") public Cat findByCatName2(String catName){ System.out.println("CatController.findByCatName2()"); return catService.findByCatName2(catName); } //http://127.0.0.1:8080/cat/selectByCatName?catName=jack1 @RequestMapping("/selectByCatName") public Cat selectByCatName(String catName){ return catService.selectByCatName(catName); } } |
新建一个类GlobalDefaultExceptionHandler,
在class注解上@ControllerAdvice,
在方法上注解上@ExceptionHandler(value=Exception.class),具体代码如下:
package com.kfit.config; import javax.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * 1、新建一个Class,这里取名为GlobalDefaultExceptionHandler * 2、在class上添加注解,@ControllerAdvice; * 3、在class中添加一个方法 * 4、在方法上添加@ExcetionHandler拦截相应的异常信息; * 5、如果返回的是View -- 方法的返回值是ModelAndView; * 6、如果返回的是String或者是Json数据,那么需要在方法上添加@ResponseBody注解. * */ @ControllerAdvice public class GlobalDefaultExceptionHandler { @ExceptionHandler(Exception.class) @ResponseBody public String defaultExceptionHandler(HttpServletRequest req,Exception e){ //是返回的String. //ModelAndView -- 介绍 模板引擎...? // ModelAndView mv=new ModelAndView(); // mv.setViewName(viewName); return "对不起,服务器繁忙,请稍后再试!"; } } |
(1)404 -- 确定地址是否输入正确,,此路径非彼路径
(2)404 -- 是否用对注解,此注解非彼注解
(3)404 -- 包路径是否正确,此包非彼包
(4)404 -- 确认类包是否正确,此类包非彼类包
1、确认访问地址是否正确: (1)确认端口号,默认是8080,这个可以在启动的控制台进行查看; (2)确认访问的URI地址是否填写正确,这个在启动的控制台查看是否被映射了。 2、确定注解是否正确,使用@RestController而不是@Controller, 另外@RestController等价于@Controller和@ResponseBody; 3、确定包的路径是否正确 我们需要知道的是:Spring Boot默认情况下可以扫描到的是 @SpringBootApplication所在的类的同包或者子包下的类。 4、 确定类引入的包的路径是否正确 @RestController:import org.springframework.web.bind.annotation.RestController @RequestMapping("/helo33"):import org.springframework.web.bind.annotation.RequestMapping |
Spring boot 默认端口是8080,如果想要进行更改的话,只需要修改applicatoin.properties文件,在配置文件中加入:
server.port=8081
在application.properties进行配置:
server.context-path=/spring-boot
访问地址就是http://ip:port/spring-boot
#server.port=8080
#server.address=# bind to a specific NIC
#server.session-timeout=# session timeout in seconds
#the context path, defaults to '/'
#server.context-path=/spring-boot
#server.servlet-path=# the servlet path, defaults to '/'
#server.tomcat.access-log-pattern=# log pattern of the access log
#server.tomcat.access-log-enabled=false # is access logging enabled
#server.tomcat.protocol-header=x-forwarded-proto # ssl forward headers
#server.tomcat.remote-ip-header=x-forwarded-for
#server.tomcat.basedir=/tmp # base dir (usually not needed, defaults to tmp)
#server.tomcat.background-processor-delay=30; # in seconds
#server.tomcat.max-threads=0 # number of threads in protocol handler
#server.tomcat.uri-encoding=UTF-8 # character encoding to use for URL decoding
在pom.xml加入thymeleaf的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
########################################################
###THYMELEAF (ThymeleafAutoConfiguration)
########################################################
#spring.thymeleaf.prefix=classpath:/templates/
#spring.thymeleaf.suffix=.html
#spring.thymeleaf.mode=HTML5
#spring.thymeleaf.encoding=UTF-8
# ;charset=<encoding> is added
#spring.thymeleaf.content-type=text/html
# set to false for hot refresh
spring.thymeleaf.cache=false
编写模板文件src/main/resouces/templates/hello.html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello.v.2</h1>
<p th:text="${hello}"></p>
</body>
</html>
<!DOCTYPEhtml> <html> <head> <metacharset="UTF-8"/> <title>Insert title here</title> </head> <body> <h1> Hello,thymeleaf <br/> This is my first thymeleaf demo. <hr/> welcome <spanth:text="${name}"></span> </h1> </body> </html> |
@Controller
public class TemplateController {
/**
* 返回html模板.
*/
@RequestMapping("/helloHtml")
public String helloHtml(Map<String,Object> map){
map.put("hello","from TemplateController.helloHtml");
return "/helloHtml";
}
}
package com.kfit; import java.util.Map; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * 注意: * 1.在Thymeleaf 模板文件中,标签是需要闭合的,3.0之前是需要闭合的 * 2. thymeleaf 3.0+ 是可以不强制要求闭合的。 * 3. 支持同时使用多个模板引擎,比如:thymeleaf和freemarker 可以并存。 */ @Controller @RequestMapping("/templates") public class TemplatesController { /** * 映射地址是:/templates/hello * @return */ @RequestMapping("/hello") public String hello(Map<String,Object> map){ map.put("name","Andy"); return "/hello"; } @RequestMapping("/hello2") public ModelAndView hello(Map<String,Object>map){ //返回的是ModelAndView对象; ModelAndView mv=new ModelAndView("hello"); mv.addObject("name","Andy"); returnmv; } @RequestMapping("/helloFtl") public String helloFtl(Map<String,Object> map){ map.put("name","Andy"); return "helloFtl"; } } |
在pom.xml中引入freemarker
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
########################################################
###FREEMARKER (FreeMarkerAutoConfiguration)
########################################################
spring.freemarker.allow-request-override=false
spring.freemarker.cache=true
spring.freemarker.check-template-location=true
spring.freemarker.charset=UTF-8
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.expose-spring-macro-helpers=false
#spring.freemarker.prefix=
#spring.freemarker.request-context-attribute=
#spring.freemarker.settings.*=
#spring.freemarker.suffix=.ftl
#spring.freemarker.template-loader-path=classpath:/templates/ #comma-separated list
#spring.freemarker.view-names=# whitelist of view names that can be resolved
<!DOCTYPE html>
<html xmlns=" 1999/xhtml" xmlns:th=" thymeleaf.org"
xmlns:sec=" /thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello.v.2</h1>
<p>${hello}</p>
</body>
</html>
@RequestMapping("/helloFtl")
public String helloFtl(Map<String,Object> map){
map.put("hello","from TemplateController.helloFtl");
return "/helloFtl";
}
使用Eclipse新建一个Maven Web Project ,项目取名为:
spring-boot-jsp
<projectxmlns=" POM/4.0.0"xmlns:xsi=" /2001/XMLSchema-instance" xsi:schemaLocation=" /POM/4.0.0 /maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.kfit</groupId> <artifactId>spring-boot-jsp</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>spring-boot-jspMavenWebapp</name> <url>http://maven.apache.org</url> <!-- spring boot parent节点,引入这个之后,在下面和spring boot相关的就不需要引入版本了; --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.1.RELEASE</version> </parent> <properties> <!-- 指定一下jdk的版本 ,这里我们使用jdk 1.8 ,默认是1.6 --> <java.version>1.8</java.version> </properties> <dependencies> <!-- web支持: 1、web mvc; 2、restful; 3、jackjson支持; 4、aop ........ --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- servlet 依赖. --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <!-- JSTL(JSP Standard Tag Library,JSP标准标签库)是一个不断完善的开放源代码的JSP标签库,是由apache的jakarta小组来维护的。 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <!-- tomcat 的支持.--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>spring-boot-jsp</finalName> </build> </project> |
添加src/main/resources/application.properties:
# 页面默认前缀目录
spring.mvc.view.prefix=/WEB-INF/jsp/
# 响应页面默认后缀
spring.mvc.view.suffix=.jsp
# 自定义属性,可以在Controller中读取
application.hello=Hello Angel From application
访问: //helloJsp
@Controller
public class HelloController {
private String hello;
@RequestMapping("/helloJsp")
public String helloJsp(Map<String,Object> map){
System.out.println("HelloController.helloJsp().hello=hello");
map.put("hello", hello);
return "helloJsp";
}
}
在 src/main 下面创建 webapp/WEB-INF/jsp 目录用来存放我们的jsp页面:helloJsp.jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"" /TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
helloJsp
<hr>
${hello}
</body>
</html>
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
注意
特别说明:针对el表达式,类似${hello} 这个对于servlet的版本是有限制的,2.4版本版本以下是不支持的,是无法进行识别的,请注意。
(1)新建maven project;
(2)在pom.xml文件中引入相关依赖;
(3)创建启动类App.java
(4)在application.properties添加配置文件;
(5)编写Demo测试类;
(6)编写DemoMapper;
(7)编写DemoService
(8)编写DemoController;
(9)加入PageHelper
(10)获取自增长ID;
新建一个maven project,取名为:spring-boot-mybatis
<projectxmlns=" /POM/4.0.0"xmlns:xsi=" /2001/XMLSchema-instance" xsi:schemaLocation=" POM/4.0.0 /xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.kfit</groupId> <artifactId>spring-boot-mybatis</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>spring-boot-mybatis</name> <url>http://maven.apache.org</url> <!-- spring boot parent节点,引入这个之后,在下面和spring boot相关的就不需要引入版本了; --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.1.RELEASE</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 指定一下jdk的版本 ,这里我们使用jdk 1.8 ,默认是1.6 --> <java.version>1.8</java.version> </properties> <dependencies> <!-- web支持: 1、web mvc; 2、restful; 3、jackjson支持; 4、aop ........ --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mysql 数据库驱动. --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- spring-boot mybatis依赖: 请不要使用1.0.0版本,因为还不支持拦截器插件, --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.2.0</version> </dependency> <!-- MyBatis提供了拦截器接口,我们可以实现自己的拦截器, 将其作为一个plugin装入到SqlSessionFactory中。 Github上有位开发者写了一个分页插件,我觉得使用起来还可以,挺方便的。 Github项目地址: https://github.com/pagehelper/Mybatis-PageHelper --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>4.1.0</version> </dependency> </dependencies> </project> |
package com.kfit.spring_boot_mybatis; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Spring Boot启动类. */ @SpringBootApplication @MapperScan("com.kfit.*")//扫描:该包下相应的class,主要是MyBatis的持久化类. public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } } |
########################################################
###datasource
########################################################
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.max-active=20
spring.datasource.max-idle=8
spring.datasource.min-idle=8
spring.datasource.initial-size=10
package com.kfit.spring_boot_mybatis; publicclass Demo { privatelongid; private String name; publiclong getId() { returnid; } publicvoid setId(longid) { this.id=id; } public String getName() { returnname; } publicvoid setName(String name) { this.name=name; } } |
package com.kfit.spring_boot_mybatis; import java.util.List; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Select; public interface DemoMappper { //#{name}:参数占位符 @Select("select * from Demo where name=#{name}") public List<Demo> likeName(String name); @Select("select * from Demo where id=#{id}") public Demo getById(long id); @Select("select name from Demo where id=#{id}") public String getNameById(long id); /** * 保存数据. */ @Insert("insert into Demo(name) values(#{name})") @Options(useGeneratedKeys=true,keyProperty="id",keyColumn="id") public void save(Demo demo); } |
package com.kfit.spring_boot_mybatis; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class DemoService { @Autowired private DemoMappper demoMappper; public List<Demo> likeName(String name){ return demoMappper.likeName(name); } @Transactional//添加事务. public void save(Demo demo){ demoMappper.save(demo); } } |
package com.kfit.spring_boot_mybatis; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.github.pagehelper.PageHelper; @RestController public class DemoController { @Autowired private DemoService demoService; @RequestMapping("/likeName") public List<Demo> likeName(String name){ /* * 第一个参数:第几页 第二个参数:每页获取的条数. */ PageHelper.startPage(1, 2); return demoService.likeName(name); } @RequestMapping("/save") public Demo save(){ Demo demo=new Demo(); demo.setName(""); demoService.save(demo); return demo; } } |
//运行访问: /likeName?name=张三 就可以看到返回的数据了
@Configuration
public class MyBatisConfiguration {
@Bean
public PageHelper pageHelper() {
System.out.println("MyBatisConfiguration.pageHelper()");
PageHelper pageHelper=new PageHelper();
Properties p=new Properties();
p.setProperty("offsetAsPageNum", "true");
p.setProperty("rowBoundsWithCount", "true");
p.setProperty("reasonable", "true");
pageHelper.setProperties(p);
return pageHelper;
}
}
@Insert("insert into Demo(name,password) values(#{name},#{password})")
public long save(Demo name);
@Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
@EnableAsync//开启异步注解功能 @EnableScheduling//开启基于注解的定时任务 @SpringBootApplication publicclass Springboot04TaskApplication { publicstaticvoid main(String[] args) { SpringApplication.run(Springboot04TaskApplication.class, args); } } |
2)代码
@Service publicclass ScheduledService { /** * second(秒), minute(分), hour(时), day of month(日), month(月), day of week(周几). * 0 * * * * MON-FRI * 【0 0/5 14,18 * * ?】 每天14点整,和18点整,每隔5分钟执行一次 * 【0 15 10 ? * 1-6】 每个月的周一至周六10:15分执行一次 * 【0 0 2 ? * 6L】每个月的最后一个周六凌晨2点执行一次 * 【0 0 2 LW * ?】每个月的最后一个工作日凌晨2点执行一次 * 【0 0 2-4 ? * 1#1】每个月的第一个周一凌晨2点到4点期间,每个整点都执行一次; */ // @Scheduled(cron="0 * * * * MON-SAT") //@Scheduled(cron="0,1,2,3,4 * * * * MON-SAT") // @Scheduled(cron="0-4 * * * * MON-SAT") @Scheduled(cron="0/4 * * * * MON-SAT") //每4秒执行一次 publicvoid hello(){ System.out.println("hello ... "); } } @Service publicclass AsyncService { //告诉Spring这是一个异步方法 @Async publicvoid hello(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("处理数据中..."); } } |
命名为demo-spring-boot-starter
下图为工程目录结构
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns=" /POM/4.0.0" xmlns:xsi=" /2001/XMLSchema-instance"
xsi:schemaLocation=" /POM/4.0.0 /xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<groupId>com.demo</groupId>
<artifactId>demo-spring-boot-starter</artifactId>
<version>0.0.1-RELEASE</version>
<name>demo-spring-boot-starter</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
@ConfigurationProperties(prefix="demo") 它可以把相同前缀的配置信息通过配置项名称映射成实体类,比如我们这里指定 prefix="demo" 这样,我们就能将以demo为前缀的配置项拿到了。
ps:其实这个注解很强大,它不但能映射成String或基本类型的变量。还可以映射为List,Map等数据结构。
package com.demo.starter.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 描述:配置信息 实体
*
* @Author shf
* @Date 2019/5/7 22:08
* @Version V1.0
**/
@ConfigurationProperties(prefix="demo")
public class DemoProperties {
private String sayWhat;
private String toWho;
public String getSayWhat() {
return sayWhat;
}
public void setSayWhat(String sayWhat) {
this.sayWhat=sayWhat;
}
public String getToWho() {
return toWho;
}
public void setToWho(String toWho) {
this.toWho=toWho;
}
}
package com.demo.starter.service;
/**
* 描述:随便定义一个Service
*
* @Author shf
* @Date 2019/5/7 21:59
* @Version V1.0
**/
public class DemoService {
public String sayWhat;
public String toWho;
public DemoService(String sayWhat, String toWho){
this.sayWhat=sayWhat;
this.toWho=toWho;
}
public String say(){
return this.sayWhat + "! " + toWho;
}
}
这里,我们将DemoService类定义为一个Bean,交给Ioc容器。
▲ @Configuration 注解就不多说了。
▲ @EnableConfigurationProperties 注解。该注解是用来开启对3步骤中 @ConfigurationProperties 注解配置Bean的支持。也就是@EnableConfigurationProperties注解告诉Spring Boot 能支持@ConfigurationProperties。
当然了,也可以在 @ConfigurationProperties 注解的类上添加 @Configuration 或者 @Component 注解
▲ @ConditionalOnProperty 注解控制 @Configuration 是否生效。简单来说也就是我们可以通过在yml配置文件中控制 @Configuration 注解的配置类是否生效。
package com.demo.starter.config;
import com.demo.starter.properties.DemoProperties;
import com.demo.starter.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 描述:配置类
*
* @Author shf
* @Date 2019/5/7 21:50
* @Version V1.0
**/
@Configuration
@EnableConfigurationProperties(DemoProperties.class)
@ConditionalOnProperty(
prefix="demo",
name="isopen",
havingValue="true"
)
public class DemoConfig {
@Autowired
private DemoProperties demoProperties;
@Bean(name="demo")
public DemoService demoService(){
return new DemoService(demoProperties.getSayWhat(), demoProperties.getToWho());
}
}
如图,新建META-INF文件夹,然后创建spring.factories文件,
在该文件中加入如下配置,该配置指定上步骤中定义的配置类为自动装配的配置。(笔者努力最近把自动装配的博客写出来)
#-------starter自动装配---------
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.demo.starter.config.DemoConfig
1000个线程并发还能跑,5000个线程的时候出现这种问题,查后台debug日志,发现redis 线程池不够。刚开始设置的是:
# redis 配置文件 #redis redis.host=127.0.0.1 redis.port=6379 redis.timeout=300 等待时间 10s改为300s redis.password=123456 redis.poolMaxTotal=1000 连接数,刚开始最大连接数 设置为100. redis.poolMaxIdle=500 最大空闲连接数 100改成500 redis.poolMaxWait=300
顺便也改了一下jdbc 的连接池参数,最大空闲和最大连接数都改成1000.在测一下。可以
spring.datasource.filters=stat spring.datasource.maxActive=1000 spring.datasource.initialSize=100 spring.datasource.maxWait=60000 spring.datasource.minIdle=500 spring.datasource.timeBetweenEvictionRunsMillis=60000 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=select 'x' spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false spring.datasource.poolPreparedStatements=true spring.datasource.maxOpenPreparedStatements=20
后来看代码发现,判断库存用的是if(stock==0 ) 抛出异常。应该用stock<0,因为 若此时同时2个线程进来,就永远小于0,后面的业务逻辑都可以执行。
第一次压力测试的时候,5000个线程,分别取不同的token(sessionId),同时访问 秒杀这个接口,商品个数只放了20个。结果出现最后商品数量变负的问题。
接口限流防刷的时候,通过计数器限流,如果超过某个阈值,向前端返回一个codeMsg对象用于显示的时候,显示的是String是乱码的问题,之前由于一直返回都是json 格式,都是封装好在data里。
这次返回是直接通过输出流直接写到response直接返回字节数组的,而不是spring controller 返回数据(springboot 默认utf-8),出现乱码问题,用utf-8编码,解决。
压测是利用Jmeter压测。(Apache开发的基于java的压测工具)。
压测具体实现:
1.在数据库中提前插入5000个用户密码(脚本 for循环 id是13000000+i),密码统一为“123456”,随机盐值也是固定的,方便操作。用JDBC存入数据库。作为5000个备用用户。
2.然后写了一个小脚本让5000个用户post请求我的登陆接口(login),生成sessionId并存入缓存,并改写了一下login接口让其换回sessionId。把这5000个用户的id和对应sessionid写到了一个TXT文件里面。
3.最后利用jmeter 创建5000个线程,账号每个线程携带提前写好的用户token(sessionId),参数就是商品id和sessionid,商品id确定我要买的票是哪个,sessionid用来获取用户信息。(从缓存中拿)
压测的瓶颈:
qps-126/s----静态化-250/s---接口优化-860/s.
瓶颈主要是对数据库的访问。
1.数据库读取,写入,处理请求的速度。
数据库读取写入加上网络IO速度很慢,减少对数据库的访问,在缓存这一端就屏蔽掉大部分访问数据库的请求(Redis预减库存操作)
2.利用消息队列,异步业务逻辑的处理速度慢,可以先返回结果,让其轮询。
3.利用内存map,减少对Redis服务器的访问,flag机制。
4.其他想到的但还没实现
服务器系统的负载均衡+集群
数据库数据达到1000W以上就很慢,分库分表
1.首先输入登陆页面的url.http://localhost:8080/login/to_login,controller根据map映射返回给html页,到达登陆页面
2.整个页面是一个login表单,包含用户名和密码两个输入框部分,还有一个登陆按钮和重置按钮。
3.在前端,给登陆按钮绑定一个login()方法,login()方法中会获取表单中的用户名和密码,然后将密码利用封装好的md5()函数以及设置的固定盐值进行拼接,盐值设置为“1a2b3c”,然后进行MD5算法生成4个32位拼接的散列值作为输入密码(用于 网络传输),作为参数传给后端。(这里的目的主要是第一道加密,防止http明文传输,泄漏密码)。
4.然后ajax异步访问do_login 接口,参数为用户名和md5之后的密码,后端接收到前端传输来的参数后,会对用户名和密码进行参数校验,验证是否为空,是否有格式问题(密码长度6位以上,用户名格式11位等等),如果验证不通过,返回CodeMsg(),封装好的对应的错误信息给前端。
5.如果验证成功,进入下一步,用户的登陆,首先通过用户名取用户对象信息(先从缓存中取,取不到取数据库取,取到了将用户信息存入缓存中,下一次登录我们可以先从缓存中取用户,降低数据库压力),然后返回一个user对象,再判断这个user对象是否为空,若是空就抛出异常,不是空的情况说明数据库中有该用户,然后根据传入的密码和数据中保存的随机盐值,进行md5再次拼接,获得的值若是和数据库中的密码一致,那么说明登陆成功。
关键点: 6.登陆成功的时候,随机生成uuid作为sessionId,将其写入cookie中返回给客户端,并且将模块前缀+该用户id作为key和sessionId 作为值,存入缓存(这里为分布式缓存提供的基础)。这时候跳转到 抢票列表页面,如果密码不匹配,抛出异常,返回。
短时间的大访问量 网站服务器 同网站,不同项目部署,/独立域名 避免对网站造成影响 高并发问题,不停刷新 数据库 页面静态化
同网站,不同项目部署,/独立域名 避免对网站造成影响 宽带 同网站,不同项目部署,/独立域名 避免对网站造成影响 不能提前下单 服务器 url动态化,+随机数
下单之后的抢的问题 sql 乐观锁
大量访问高并发的应对(主要访问大量访问数据库崩溃)
1.Redis预减库存减少数据库访问
2.map标记减少Redis访问屏蔽一定的请求减轻缓存压力
3.消息队列异步处理
流量削峰 开始抢购的瞬间 大量并发进入,先将请求入队,若队列满了,那么舍弃再入队的请求返回一个异常
先给前端一个数据返回表示排队中,再进行后续的业务处理,前端轮询最后成功或者失败在显示业务结果
4.数据库运行的问题,传统的sql写成存储过程(直接调用),加速sql
5.数据库里锁及唯一索引来处理抢的问题。
页面加载速度
页面静态化,缓存在客户端
CDN服务器
在上表中列出来的解决方案中看出,利用 页面静态化、数据静态化,反向代理 等方法可以避免 带宽和sql压力 ,但是随之而来一个问题,页面抢单按钮也不会刷新了,可以把 js 文件单独放在js服务器上,由另外一台服务器写 定时任务 来控制js 推送。
另外还有一个问题,js文件会被大部分浏览器缓存,我们可以使用xxx.js?v=随机数 的方式来避免js被缓存
更为激进的缓存方式(之前可以用将html源码缓存起来再读,避免服务器渲染html过程)。
什么是浏览器缓存:
简单来说,浏览器缓存就是把一个已经请求过的Web资源(如html页面,图片,js,数据等)拷贝一份副本储存在浏览器中。缓存会根据进来的请求保存输出内容的副本。当下一个请求来到的时候,如果是相同的URL,缓存会根据缓存机制决定是直接使用副本响应访问请求,还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页,当再次访问这个URL地址的时候,如果网页没有更新,就不会再次下载网页,而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新,浏览器才会再次下载网页。
页面静态化的好处:
我们知道浏览器会将html,图片等静态数据,缓存到本地,在高并发抢票场景,用户会通过不断的刷新页面来进行抢票操作,这样带来Web带宽的浪费以及服务器的访问压力。于是,我们可以通过将抢票页面做成静态页面html页,其中的票务数据通过ajax异步调用接口来获取,仅仅交互的是部分数据,减少了带宽,也加快用户访问的速度。
function getDetail() { var goodsId=g_getQueryString("goodsId"); $.ajax({ url : "/goods/to_detail/"+goodsId, type : "GET", success: function (data) { if (data.code==0) {// 访问后端detail 接口拿到数据 render(data.data);//渲染界面的方法 }else { layer.msg(data.msg) } }, error:function () { layer.msg("客户端请求有误!") } }) } function render(detail) { var goodsVo=detail.goodsVo; var miaoshaStatus=detail.miaoshaStatus; var remainSeconds=detail.remainSeconds; var user=detail.user; if (user) { $("#userTip").hide();//没有就不展示 } //用获取的参数 放入 对应的模板中 $("#goodsName").text(goodsVo.goodsName); $("#goodsImg").attr("src", goodsVo.goodsImg); $("#startTime").text(new Date(goodsVo.startDate).format("yyyy-MM-dd hh:mm:ss")); $("#remainSeconds").val(remainSeconds); $("#goodsId").val(goodsVo.id); $("#goodsPrice").text(goodsVo.goodsPrice); $("#miaoshaPrice").text(goodsVo.miaoshaPrice); $("#stockCount").text(goodsVo.stockCount); countDown();//调用倒计时 } function countDown() { var remainSeconds=$("#remainSeconds").val(); // var remainSeconds=$("#remainSeconds").val(); var timeout;//定义一个timeout 保存Timeout 值 if (remainSeconds>0){//秒杀未开始 $("#buyButton").attr("disabled",true);/*还没开始的时候按钮不让点*/ $("#miaoshaTip").html("秒杀倒计时:"+remainSeconds+"秒"); /*且做一个倒计时*/ timeout=setTimeout(function () {//setTimeout 为时间到了之后执行 该函数 $("#countDown").text(remainSeconds-1);//将显示中的值 -1 $("#remainSeconds").val(remainSeconds-1);// remianSeconds 值减一 countDown();//在调用该方法 实现循环 },1000) }else if (remainSeconds==0){//秒杀进行中 $("#buyButton").attr("disabled",false); //当remainSeconds=0 clearTimeout(timeout);//取消timeout 代码执行 $("#miaoshaTip").html("秒杀进行中!")//修改其中的内容 /**加入秒杀数学验证码 功能 * 1.一开始图形验证码和输入框都是隐藏的 * 2.当秒杀进行的时候,显示验证码和输入框 * */ $("#verifyCodeImg").attr("src", "/miaosha/verifyCode?goodsId="+$("#goodsId").val());//访问验证码接口 $("#verifyCodeImg").show(); $("#verifyCode").show(); } else {//秒杀结束 $("#buyButton").attr("disabled",true); $("#miaoshaTip").html("结束!!!")//修改其中的内容 } }
做法:首先将票务详情这个template 模板 html页放在static 文件下,然后改掉thymeleaf 模板语言标签让其成为纯html语言,然后将票务列表中的链接指向(本来是requestMapping,向后端contrller 请求这个详情业务及数据,然后利用spring渲染模板,在返回的),现在直接指向static文件下的票务详情页(链接中带商品id作为参数),最后在这个html页面写ajax异步访问后端接口/getdetail,后端接口也改造一下返回的是这个商品的全部详细信息,封装在data里,以json的形式,然后写了一个render(),把从后端传来的数据写进对应数据中。
/** 页面静态化:商品详情页面 * 方法:返回的是一个静态html 页面 + 利用ajax(通过接口)从服务端获取对应数据 + js技术将数据放入html * */ @RequestMapping(value="/to_detail/{goodsId}") // 前端传入的参数 goodsId @ResponseBody public Result<GoodsDetailVo> detail(HttpServletRequest request, HttpServletResponse response, Model model, MiaoshaUser user, @PathVariable("goodsId") Long goodsId){//通过注解@PathVariable获取路径参数 /*先将user 传进去 用来判断是否登录*/ model.addAttribute("user",user); /*根据传入的Id 通过service 拿到对应的Good信息*/ GoodsVo goods=goodsService.getGoodsById(goodsId); model.addAttribute("goods",goods); long startTime=goods.getStartDate().getTime(); long endTime=goods.getEndDate().getTime(); long nowTime=System.currentTimeMillis();/* 拿到现在时间的毫秒值*/ /**这里要做一个秒杀时间的判断 秒杀开始 秒杀结束 秒杀进行 * */ int miaoshaStatus=0;/*用该变量来表示 秒杀的状态 0 表示秒杀未开始 1 开始 2 结束*/ int remainSeconds=0; /*表示剩余时间 距离秒杀开始的时间*/ if (nowTime<startTime){//秒杀未开始 miaoshaStatus=0; remainSeconds=(int)((startTime-nowTime)/1000);//注意此时是 毫秒值 要除以1000 }else if (endTime<nowTime){//秒杀结束 miaoshaStatus=2; remainSeconds=-1; }else {//秒杀进行中 miaoshaStatus=1; remainSeconds=0; } model.addAttribute("remainSeconds",remainSeconds); model.addAttribute("miaoshaStatus",miaoshaStatus); /* 将我们需要的数据 封装到GoodsDetailVo中 */ GoodsDetailVo goodsDetailVo=new GoodsDetailVo(); goodsDetailVo.setGoodsVo(goods); goodsDetailVo.setMiaoshaStatus(miaoshaStatus); goodsDetailVo.setRemainSeconds(remainSeconds); goodsDetailVo.setUser(user); return Result.success(goodsDetailVo);
针对于上面所涉及到的知识点我总结出了有1到5年开发经验的程序员在面试中涉及到的绝大部分架构面试题及答案做成了文档和架构视频资料免费分享给大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术资料),希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习,也可以关注我一下以后会有更多干货分享。
个 登 录 有 必 要 做 成 那 样 吗 ? \textcolor{green}{一个登录有必要做成那样吗?}一个登录有必要做成那样吗?我 的 回 答 : 非 常 有 必 要 \textcolor{red}{我的回答:非常有必要}我的回答:非常有必要
试 想 一 下 , 如 果 一 个 网 站 可 以 随 便 进 去 点 赞 等 操 作 , 那 还 要 登 录 干 什 么 ? \textcolor{green}{试想一下,如果一个网站可以随便进去点赞等操作,那还要登录干什么?}试想一下,如果一个网站可以随便进去点赞等操作,那还要登录干什么?
博 主 也 在 学 习 阶 段 , 如 若 发 现 问 题 , 请 告 知 , 非 常 感 谢 \textcolor{Orange}{博主也在学习阶段,如若发现问题,请告知,非常感谢}博主也在学习阶段,如若发现问题,请告知,非常感谢
周 榜 也 到 了 26 , 哇 塞 ( o ゜ ▽ ゜ ) o ☆ , 顺 便 提 一 下 今 天 库 里 必 登 三 分 历 史 第 一 \textcolor{blue}{周榜也到了26,哇塞(o゜▽゜)o☆,顺便提一下今天库里必登三分历史第一}周榜也到了26,哇塞(o゜▽゜)o☆,顺便提一下今天库里必登三分历史第一
SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。
拦截器和过滤器的区别在于拦截器使AOP思想的具体应用
? ? > 新 建 一 个 M o d u l e , 添 加 w e b 支 持 \textcolor{OrangeRed}{--> 新建一个Module,添加web支持}??>新建一个Module,添加web支持
? ? > 配 置 w e b . x m l , a p p l i c a t i o n C o n t e x t . x m l , 添 加 一 个 c o n t r o l l e r 的 包 \textcolor{OrangeRed}{--> 配置web.xml,applicationContext.xml,添加一个controller的包}??>配置web.xml,applicationContext.xml,添加一个controller的包
? ? > 编 写 测 试 \textcolor{OrangeRed}{--> 编写测试}??>编写测试
@RestController
public class TestController {
@GetMapping("/t1")
public String test(){
System.out.println("TestController-->test()执行了");
return "ok";
}
}
12345678
添加Artifact中的lib,以及配置Tomcat,启动测试出现,证明Spring配置好了
? ? > 编 写 拦 截 器 \textcolor{OrangeRed}{--> 编写拦截器}??>编写拦截器
package com.hxl.config;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {
//在请求处理的方法之前执行
//return true;执行下一个拦截器
//如果返回false就不执行下一个拦截器
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("------------处理前------------");
return true;
}
//在请求处理方法执行之后执行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("------------处理后------------");
}
//在dispatcherServlet处理后执行,做清理工作.
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("------------清理------------");
}
}
12345678910111213141516171819202122232425
? ? > 在 a p p l i c a t i o n C o n t e x t . x m l 中 配 置 拦 截 器 \textcolor{OrangeRed}{--> 在applicationContext.xml中配置拦截器}??>在applicationContext.xml中配置拦截器
<!--关于拦截器的配置-->
<mvc:interceptors>
<mvc:interceptor>
<!--/** 包括路径及其子路径-->
<!--/admin/* 拦截的是/admin/add等等这种 , /admin/add/user不会被拦截-->
<!--/admin/** 拦截的是/admin/下的所有-->
<mvc:mapping path="/**"/>
<!--bean配置的就是拦截器-->
<bean class="com.hxl.config.MyInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
1234567891011
前面的我们都不动,运行,我们可以看到效果
那么接下来就用一个实例来体验一下拦截器(登录)
在WEB-INF下的所有页面或者资源,只能通过controller或者servlet进行访问
? ? > i n d e x . j s p \textcolor{OrangeRed}{--> index.jsp}??>index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<a href="${pageContext.request.contextPath}/goLogin">登录</a>
<a href="${pageContext.request.contextPath}/goMain">首页</a>
</body>
</html>
123456789101112
? ? > m a i n . j s p \textcolor{OrangeRed}{--> main.jsp}??>main.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>首页</h1>
</body>
</html>
1234567891011
? ? > l o g i n . j s p \textcolor{OrangeRed}{--> login.jsp}??>login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录</title>
</head>
<body>
<h1>登录页面</h1>
<form action="${pageContext.request.contextPath}/login" method="post">
用户名:<input type="text" name="username">
密码:<input type="text" name="password">
<input type="submit" value="登录">
</form>
</body>
</html>
12345678910111213141516
? ? > L o g i n C o n t r o l l e r \textcolor{OrangeRed}{--> LoginController}??>LoginController
package com.hxl.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpSession;
@Controller
public class LoginController {
@RequestMapping("/goMain")
public String goMain(){
return "main";
}
@RequestMapping("/goLogin")
public String goLogin(){
return "login";
}
@RequestMapping("/login")
public String login(HttpSession session,String username,String password){
// 向session记录用户身份信息
System.out.println("接收前端==="+username);
session.setAttribute("user", username);
return "main";
}
}
12345678910111213141516171819202122232425262728
? ? > 测 试 : \textcolor{OrangeRed}{--> 测试:}??>测试:
因为在WEB-INF下的 页面我们不能直接访问,所以在index中进行跳转,然后请求到login.jsp。此时我们在这里输入,就会在session中携带账号密码。
但此时是不符合的,因为我们还没有登录就可以去首页,所以我们需要写一个拦截器的功能
? ? > i n d e x . j s p \textcolor{OrangeRed}{--> index.jsp}??>index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<a href="${pageContext.request.contextPath}/user/goLogin">登录</a>
<a href="${pageContext.request.contextPath}/user/goMain">首页</a>
</body>
</html>
123456789101112
? ? > l o g i n . j s p \textcolor{OrangeRed}{--> login.jsp}??>login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录</title>
</head>
<body>
<h1>登录页面</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
用户名:<input type="text" name="username">
密码:<input type="text" name="password">
<input type="submit" value="登录">
</form>
</body>
</html>
12345678910111213141516
? ? > m a i n . j s p \textcolor{OrangeRed}{--> main.jsp}??>main.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p>${username}</p>
<p>
<a href="${pageContext.request.contextPath}/user/goOut">注销</a>
</p>
</body>
</html>
1234567891011121314
? ? > L o g i n I n t e r c e p t o r \textcolor{OrangeRed}{--> LoginInterceptor}??>LoginInterceptor
此时我们去写一个登录的拦截器,来判断它到底什么时候进行拦截
package com.hxl.config;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session=request.getSession();
//放行:判断什么情况下没有登录
//登录页面放行
if(request.getRequestURI().contains("goLogin")){
return true;
}
if(request.getRequestURI().contains("login")){
return true;
}
//用户已登录,第一次登录的时候也是没有session的。
if(session.getAttribute("user") !=null){
return true;
}
//判断什么情况下没有登录
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);
return false;
}
}
12345678910111213141516171819202122232425262728
? ? > L o g i n C o n t r o l l e r \textcolor{OrangeRed}{--> LoginController}??>LoginController
这里面我们有一个类url,下面的请求都需要加上/user,在配置拦截器的时候可以加一个,只拦截user请求下的。以及注销功能
package com.hxl.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpSession;
@Controller
@RequestMapping("/user")
public class LoginController {
@RequestMapping("/goMain")
public String goMain(){
return "main";
}
@RequestMapping("/goLogin")
public String goLogin(){
return "login";
}
@RequestMapping("/login")
public String login(HttpSession session, String username, String password, Model model){
// 向session记录用户身份信息
System.out.println("接收前端==="+username);
session.setAttribute("user", username);
model.addAttribute("username",username);
return "main";
}
@RequestMapping("/goOut")
public String goOut(HttpSession session){
/*//销毁,下面的好
session.invalidate();*/
//移除
session.removeAttribute("user");
return "main";
}
}
12345678910111213141516171819202122232425262728293031323334353637383940
? ? > a p p l i c a t i o n C o n t e x t . x m l \textcolor{OrangeRed}{-->applicationContext.xml}??>applicationContext.xml
<!--关于拦截器的配置-->
<mvc:interceptors>
<mvc:interceptor>
<!--/** 包括路径及其子路径-->
<!--/admin/* 拦截的是/admin/add等等这种 , /admin/add/user不会被拦截-->
<!--/admin/** 拦截的是/admin/下的所有-->
<mvc:mapping path="/**"/>
<!--bean配置的就是拦截器-->
<bean class="com.hxl.config.MyInterceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<!--user下面的请求-->
<mvc:mapping path="/user/**"/>
<bean class="com.hxl.config.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
1234567891011121314151617
? ? > 测 试 \textcolor{OrangeRed}{-->测试}??>测试
此时我们启动之后,如果没有登录,那么会重定向到goLogin页面,然后登录,登录之后跳转到main页面,其中有注销功能,没有注销之前可以去index页面点击首页正常跳转,如果注销了,session没有了,那么就会跳转到登录页面。
*请认真填写需求信息,我们会在24小时内与您取得联系。