第一个SpringBoot程序
第一个springboot程序
使用idea新建工程,选择springboot initializr,勾选web一路next就搭建了一个最简单的springboot工程。如下:
package com.shy.springboot; import org.springframework.boot.springapplication; import org.springframework.boot.autoconfigure.springbootapplication; @springbootapplication public class springbootapplication { public static void main(string[] args) { springapplication.run(springbootapplication.class, args); } }
@springbootapplication整合了三个常用的注解,分别是:
- @componentscan:会自动扫描指定包下的全部标有@component的类,并注册成bean,当然包括@component下的子注解@service,@repository,@controller;
- @springbootconfiguration:可以当成spring的标准配置注解@configuration来使用。而@configuration表明这是一个javaconfig配置类。通常配合@bean注解,@bean注解告诉spring这个方法将返回一个对象,该对象将会注册为spring应用上下文中的bean;
- @enableautoconfiguration:能够自动配置spring的上下文,试图猜测和配置你想要的bean类,通常会自动根据你的类路径和你的bean定义自动配置。
配置文件相关
springboot的配置文件可以使用xml和yml格式,比如使用yml格式
# 自定义属性 cupsize: b age: 18 # 可以在yml里通过${}来引用 content: "cupsize: ${cupsize}, age: ${age}" # 指定端口为8080,(不配置默认8080) server: port: 8080
可以使用注解@value("${...}")
获取配置文件中的值,@value和@autowired注解作用类似。
spring提供了两种在运行时求值的方式:
-
属性占位符:
${...}
-
spring表达式语言(spel):
#{...}
如果cupsize和age都是属于同一类属性下的子属性,比如都属于girl。
那么可以写成下面的形式:
girl: cupsize: b age: 18
在java中注入时,也不用一个个属性注入,可以注入girl的全部属性。不过需要将girl的属性抽象成一个java类。
package com.shy.springboot.config; import org.springframework.boot.context.properties.configurationproperties; import org.springframework.stereotype.component; /** * 读取配置文件的信息并自动封装成实体类 * 注入springboot配置文件中前缀是"girl"的全部属性 */ @component @configurationproperties(prefix = "girl") public class girlproperties { private string cupsize; private integer age; public string getcupsize() { return cupsize; } public integer getage() { return age; } }
属性配置方式
- @value,从配置文件中注入属性
- @configurationproperties(prefix = "...")读取配置文件中对应前缀中的属性,并映射成对象实体
环境配置:
可以建立多个application-xxx.yml文件,然后在application.yml文件中配置其中一个环境.
比如我有application-dev.yml文件表示开发环境下的配置文件,application-prod.yml文件表示生产环境下的配置文件。那么再按application.yml中配置如下
spring: profiles: active: prod
就表示使用application-dev.yml中的配置。
一些常用注解
- controller,作用于类上,表示mvc中的控制层,可以被@componentscan扫描到并注入。用于处理http请求,返回字符串代表的模板,如xx.jsp, xx.ftl。
- @restcontroller,是@responsebody和@controller的整合,处理http请求,可以返回实体对象或字符串,以json格式表示。
- @resqustmapping,配置url映射。可以在类上使用(作为类中方法的前缀),可以在方法上使用。
package com.shy.springboot.controller; import com.shy.springboot.config.girlproperties; import org.springframework.beans.factory.annotation.autowired; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.requestmethod; import org.springframework.web.bind.annotation.restcontroller; @restcontroller @requestmapping("/girl") public class hello { /** * 表示value中的值都可作为url路径,如果不指定请求方法method,那么get和post方式都可以,但是一般不推荐 */ @requestmapping(value = {"/hello", "/hi"}, method = requestmethod.get) public string hello() { return "hello"; } }
- @pathvariable,获取url路径中的数据
- @requstparam,获取请求参数中的值
- @getmapping,组合注解,是@requestmapping(value = "...", method = requestmethod.get)的缩写形式,当然也有postmapping了。
package com.shy.springboot.controller; import com.shy.springboot.config.girlproperties; import org.springframework.beans.factory.annotation.autowired; import org.springframework.web.bind.annotation.*; @restcontroller @requestmapping("/girl") public class hello { // 可以响应 http://localhost:8080/girl/hello/xx @requestmapping(value = {"/hello/{id}"}, method = requestmethod.get) public string hello(@pathvariable("id") integer id) { return "my id is " + id; } // 可以响应 http://localhost:8080/girl/hello?id=xx // required = false表示这个参数可以为空,defaultvalue表示当参数为空时的默认值,因此访问http://localhost:8080/girl/hello,将使用默认值1。 @requestmapping(value = {"/hello"}, method = requestmethod.get) public string hello2(@requestparam(value = "id", required = false,defaultvalue = "1") integer id) { return "my id is " + id; } }
数据库配置
本例子使用jpa和mysql,所以在pom中加入如下依赖
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-jpa</artifactid> </dependency> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> </dependency>
在application.yml中配置数据源和jpa相关。
spring: profiles: active: dev # 以下使用了jpa和mysql datasource: driver-class-name: com.mysql.jdbc.driver url: jdbc:mysql://127.0.0.1:3306/dbgirl username: root password: admin jpa: hibernate: ddl-auto: create show-sql: true
jpa(java persistence api),即java持久化api,hibernate实现了这个规范。
package com.shy.springboot.database; import javax.persistence.entity; import javax.persistence.generatedvalue; import javax.persistence.id; @entity public class girl { @id @generatedvalue(strategy = generationtype.identity) private integer id; private integer age; private string cupsize; public integer getid() { return id; } public void setid(integer id) { this.id = id; } public integer getage() { return age; } public void setage(integer age) { this.age = age; } public string getcupsize() { return cupsize; } public void setcupsize(string cupsize) { this.cupsize = cupsize; } public girl() { } }
- @entity 表示这是个实体类,可以映射成数据表。
- @id表示该属性为主键
- @generatedvalue表示该属性字段为自增,一般搭配@id使用
@generatedvalue有几种策略
- identity:采用数据库id自增长的方式来自增主键字段,oracle 不支持这种方式,使用mysql时,配置该策略可以实现主键自增。
- auto:jpa自动选择合适的策略,是默认选项;
- sequence:通过序列产生主键,通过@sequencegenerator 注解指定序列名,mysql不支持这种方式 ;
- table:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植;
jpa.hibernate.ddl-auto
,共有五种配置方式。
- ddl-auto:create: 每次运行该程序,没有表格会新建表格,表内有数据会清空
- ddl-auto:create-drop: 每次程序结束的时候会清空表
- ddl-auto:update: 每次运行程序,没有表格会新建表格,若已经存在表格,则只会更新
- ddl-auto:validate: 运行程序会校验数据与数据库的字段类型是否相同,不同会报错
- none: 禁止ddl处理
controller和几个简单的请求
repository提供了最基本的数据访问功能,通过新建一个接口继承jparepository<t, id>
,可以直接使用接口中现成的方法来实现对数据的访问。
package com.shy.springboot.database; import org.springframework.data.jpa.repository.jparepository; import java.util.list; public interface girlrepo extends jparepository<girl, integer> { // 自定义的查询方法,方法名要严格按照一定规则来命名 list<girl> findbyage(integer age); }
泛型中的girl表示该repository可以访问由girl映射的数据表,integer表示id的数据类型。
写一个controller,处理各种请求来看jpa是如何与数据库交互的。
package com.shy.springboot.controller; import com.shy.springboot.database.girl; import com.shy.springboot.database.girlrepo; import org.springframework.beans.factory.annotation.autowired; import org.springframework.web.bind.annotation.*; import java.util.list; @restcontroller public class girlcontroller { @autowired private girlrepo girlrepo; /** * 查询所有女生 * @return */ @getmapping("/girls") public list<girl> girls() { return girlrepo.findall(); } /** * 添加一个女生 * @param cupsize * @param age * @return */ @postmapping("/addgirl") public girl addgirl(@requestparam("cupsize") string cupsize, @requestparam("age") integer age) { girl girl = new girl(); girl.setage(age); girl.setcupsize(cupsize); return girlrepo.save(girl); } /** * 通过id更新一个女生 * @param id * @param cupsize * @param age * @return */ @postmapping("/updategirl/{id}") public girl updategirl(@pathvariable("id") integer id, @requestparam("cupsize") string cupsize, @requestparam("age") integer age) { girl girl = new girl(); girl.setid(id); girl.setage(age); girl.setcupsize(cupsize); return girlrepo.save(girl); } /** * 根据id删除一个女生 * @param id */ @getmapping("/deletegirl/{id}") public void deletegirl(@pathvariable("id") integer id) { girl girl = new girl(); girl.setid(id); girlrepo.delete(girl); } /** * 根据id查询一个女生 * @param id * @return */ @getmapping("girls/{id}") public girl girlfindone(@pathvariable("id") integer id) { return girlrepo.findbyid(id).get(); } /** * 根据年龄查询一个女生 * @param age * @return */ @getmapping("girls/age/{age}") public list<girl> findgirlsbyage(@pathvariable("age") integer age) { return girlrepo.findbyage(age); } }
没有写一句sql语句,就完成了对girl表的增删改查,用起来还是很舒服的。
事务管理
下面的inserttwo方法插入两条数据,如果不进行事务管理,则插入girla成功,插入girlb失败。加上@transactional注解后(有两个同名注解,导入spring的),要么两条数据都插入成功,要么两条都插入失败。因为在本例中会出现异常,所以两条都插入失败。
// service中 package com.shy.springboot.service; import com.shy.springboot.database.girl; import com.shy.springboot.database.girlrepo; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.service; import org.springframework.transaction.annotation.transactional; @service public class girlservice { @autowired private girlrepo girlrepo; @transactional public void inserttwo() { girl girla = new girl(); girla.setcupsize("b"); girla.setage(18); girlrepo.save(girla); // 除0异常,退出 int a = 3 / 0; girl girlb = new girl(); girlb.setcupsize("c"); girlb.setage(20); girlrepo.save(girlb); } } // controller中 @postmapping("/girls/inserttwo") public void inserttwo() { girlservice.inserttwo(); }
因为hibernate创建的表默认引擎是myisam,所以如果发现事务没有作用,要手动修改引擎为innodb。
alter table xxx engine=innodb;
表单验证
在上面的例子中如果要对年龄作限制,比如小于18岁的girl不能添加。可以在实体类中对其中的字段属性使用注解来加以限制。
@min(value = 18, message = "未满18岁不得入内!") private integer age;
这句代码限制了girl的年龄不能低于18岁。在controller中修改添加女生的逻辑
/** * 添加一个女生 * @return */ @postmapping("/addgirl") public girl addgirl(@valid girl girl, bindingresult result) { if (result.haserrors()) { system.out.println(result.getfielderror().getdefaultmessage()); return null; } return girlrepo.save(girl); }
@valid可以对对象进行验证,加了@valid注解的参数,其后要紧跟着bindingresult或者errors(前者是后者的实现类),用于保存验证结果。如果对象中有属性不满足验证条件,其结果将体现中bindingresult中。
aop
首先在pom中添加依赖
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-aop</artifactid> </dependency>
然后编写切面
package com.shy.springboot.aspect; import org.aspectj.lang.annotation.after; import org.aspectj.lang.annotation.aspect; import org.aspectj.lang.annotation.before; import org.aspectj.lang.annotation.pointcut; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.stereotype.component; @aspect @component public class httpaspect { private static final logger log = loggerfactory.getlogger(httpaspect.class); @pointcut("execution(public * com.shy.springboot.controller.girlcontroller.*(..))") public void log() {} @before("log()") public void dobefore() { log.info("我在方法调用前执行"); } @after("log()") public void doafter() { log.info("我在方法调用后执行"); } }
因为在方法调用的前后都要对相同的方法进行通知,为了避免代码冗余,把@before和@after的execution表达式抽取成切点。
@pointcut("execution(public * com.shy.springboot.controller.girlcontroller.*(..))")
表示对girlcontroller中所有public的任意返回值、任意参数的方法进行通知。注意该注解需要用在方法上,所以public log() {}
在这里只是起一个标识作用,供@pointcut依附,所以它的方法体是空的。
该切面使用了slf4j的日志。当请求http://localhost:8080/addgirl时,控制台输出以下日志,可以显示比system.out.println()
更详细的信息。
2018-10-03 10:03:46.899 info 1892 --- [nio-8080-exec-3] com.shy.springboot.aspect.httpaspect : 我在方法调用前执行 hibernate: insert into girl (age, cup_size) values (?, ?) 2018-10-03 10:03:47.031 info 1892 --- [nio-8080-exec-3] com.shy.springboot.aspect.httpaspect : 我在方法调用后执行
输出的hibernate: insert into girl (age, cup_size) values (?, ?)
表示了controller中addgirl方法的执行,在其前后分别输出了@before和@after执行的逻辑,所以aop确实是生效了的。
现在修改dobefore方法,使它能从request域中获取请求url、ip地址、请求方法、请求中传递的参数。
@aspect @component public class httpaspect { private static final logger log = loggerfactory.getlogger(httpaspect.class); @pointcut("execution(public * com.shy.springboot.controller.girlcontroller.*(..))") public void log() {} @before("log()") public void dobefore(joinpoint joinpoint) { servletrequestattributes attributes = (servletrequestattributes) requestcontextholder.getrequestattributes(); httpservletrequest request = attributes.getrequest(); // url log.info("url={}", request.getrequesturi()); // ip log.info("ip={}", request.getremoteaddr()); // method log.info("method={}", request.getmethod()); // 参数 log.info("args={}", joinpoint.getargs()); // class-method log.info("class_method={}", joinpoint.getsignature().getdeclaringtypename() + " " + joinpoint.getsignature().getname()); log.info("我在方法调用前执行"); } @afterreturning(value = "log()",returning = "obj") public void doafterreturning(object obj) { if (obj != null) { log.info("girl={}", obj.tostring()); } } @after("log()") public void doafter() { log.info("我在方法调用后执行"); } }
在通知方法中可以声明一个joinpoint类型的参数,通过joinpoint可以访问连接点的细节。
- getargs():获取连接点方法运行时的入参列表;
- getsignature() :获取连接点的方法签名对象;
- getsignature().getname():获取连接点的方法名
- getsignature().getdeclaringtypename():获取连接点所在类的名称
还新增了一个@afterreturning的通知,在方法成功返回后执行(若抛出异常将不会执行该通知),和@after的区别在于:被增强的方法不论是执行成功还是抛出异常,@after通知方法都会得到执行。
aop中 @before @after @afterthrowing @afterreturning的执行顺序如下:
public object invoke(object proxy, method method, object[] args) throws throwable { object result; try { // @before result = method.invoke(target, args); // @after return result; } catch (invocationtargetexception e) { throwable targetexception = e.gettargetexception(); // @afterthrowing throw targetexception; } finally { // @afterreturning } }
可知@afterreturning的执行在@after之后。
如果请求http://localhost:8080/addgirl,将输出以下日志(日志一些无关紧要的内容已被删除)
url=/addgirl ip=0:0:0:0:0:0:0:1 method=post args=girl{id=null, age=26, cupsize='c'} class_method=com.shy.springboot.controller.girlcontroller addgirl 我在方法调用前执行 hibernate: insert into girl (age, cup_size) values (?, ?) 我在方法调用后执行 girl=girl{id=27, age=26, cupsize='c'}
统一异常处理
前面的addgirl方法,当验证不通过时,返回null并在控制台打印相关信息;当验证通过又返回girl。返回值不统一,而且如果我们希望将错误信息显示在页面,怎么办呢?
可定义一个result<t>
,将要呈现的信息统一化,分别是错误码code,错误信息msg和承载的对象t,这样不管是成功还是发生各种各样的异常,都可以返回统一的result对象。
package com.shy.springboot.domain; public class result<t> { /** 错误码 */ private integer code; /** 信息 */ private string msg; /** 对象 */ private t data; public integer getcode() { return code; } public void setcode(integer code) { this.code = code; } public string getmsg() { return msg; } public void setmsg(string msg) { this.msg = msg; } public t getdata() { return data; } public void setdata(t data) { this.data = data; } }
再写一个工具类,可在成功和异常时候设置对应的状态和信息,可有效减少重复代码。
package com.shy.springboot.util; import com.shy.springboot.domain.result; public class resultutil { public static result success(object obj) { result result = new result(); result.setmsg("成功"); result.setcode(0); result.setdata(obj); return result; } public static result success() { return success(null); } public static result error(integer code, string msg) { result result = new result(); result.setmsg(msg); result.setcode(code); return result; } }
于是我们的addgirl方法可以重构成下面的样子
@postmapping("/addgirl") public result<girl> addgirl(@valid girl girl, bindingresult bindingresul) { if (bindingresul.haserrors()) { return resultutil.error(1,bindingresul.getfielderror().getdefaultmessage()); } girlrepo.save(girl); return resultutil.success(girl); }
现在新增一个检查年龄的逻辑,小于14岁的认为在上小学,14~17岁认为在上初中,这两种情况都不允许其进入,当检查到年龄不符合要求时,抛出异常。
在girlservice中
public void checkage(integer id) { girl girl = girlrepo.findbyid(id).get(); int age = girl.getage(); if (age < 14) { throw new girlexception(resultenum.primary_school); } else if (age < 17) { throw new girlexception(resultenum.middle_school); } // 其他年龄的逻辑处理 }
注意上面使用枚举来统一管理各种code对应的msg。
package com.shy.springboot.enums; public enum resultenum { success(0, "成功"), error(-1, "未知错误"), primary_school(100, "你可能还在上小学"), middle_school(101, "你可能还在上初中"); private integer code; private string msg; resultenum(integer code, string msg) { this.code = code; this.msg = msg; } public integer getcode() { return code; } public string getmsg() { return msg; } }
girlexception是个自定义异常类,除了message还把code整合进去了。
package com.shy.springboot.exception; import com.shy.springboot.enums.resultenum; public class girlexception extends runtimeexception{ private integer code; public girlexception(resultenum resultenum) { super(resultenum.getmsg()); this.code = resultenum.getcode(); } public integer getcode() { return code; } public void setcode(integer code) { this.code = code; } }
在controller中只是简单调用下service中的方法而已
@getmapping("/girlage/{id}") public void getage(@pathvariable("id") integer id) { girlservice.checkage(id); }
如果现在启动程序,请求http://localhost:8080/girlage/22, 将按照自定义异常,但是返回的结果其格式是下面这样的:
{ timestamp: 14xxxxxxx, status: 500, exception: xxx, message: xxx, path: "/girlage/22" }
因为系统内部发生了错误,不断往上抛异常就会得到上面的信息。如果要保持不管在什么情况下统一返回result<t>
中的信息,像下面这样:
{ code: xxx, msg: xxx, data: xxx }
则需要对异常做一个捕获,取出有用的message部分,然后再封装成result对象,再返回给浏览器。为此新建一个异常捕获类
package com.shy.springboot.handle; import com.shy.springboot.domain.result; import com.shy.springboot.exception.girlexception; import com.shy.springboot.util.resultutil; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.web.bind.annotation.controlleradvice; import org.springframework.web.bind.annotation.exceptionhandler; import org.springframework.web.bind.annotation.responsebody; /** * controlleradvice注解将作用在所有注解了@requestmapping的控制器的方法上。 * 配合@exceptionhandler,用于全局处理控制器里的异常 */ @controlleradvice public class exceptionhandle { private static final logger log = loggerfactory.getlogger(exceptionhandle.class); @exceptionhandler(exception.class) @responsebody public result handle(exception e) { if (e instanceof girlexception) { girlexception exception = (girlexception) e; return resultutil.error(exception.getcode(),exception.getmessage()); } log.error("系统异常:{}", e.getmessage()); return resultutil.error(-1,"未知错误"); } }
该类使用了注解@controlleradvice,@controlleradvice会作用在所有注解了@requestmapping的控制器的方法上,再配合@exceptionhandler,用于全局处理控制器里的异常。@exceptionhandler(exception.class)表示可以处理exception类及其子类。
因为除了会抛出自定义异常girlexception外,还有可能因为系统原因抛出其他类型的异常(如空指针异常),因此针对不同类型的异常返回不同的状态码,上面使用了instanceof来判断异常类型。如果不是girlexception,被统一归类为未知错误,但是各种异常都显示未知错误不便于排查问题,因此在可控制台输出了异常原因来加以区分。
单元测试
springboot中进行单元测试十分便捷,springboot中默认使用了junit4。
在src/test下可以创建单元测试类,当然更简单的方法是在idea下右键,go to -> test subject,然后选择想要进行测试的方法即可。
下面的单元测试针对service层,主要是判断某数据库中某id的girl,其年龄实际值和预期值是否一致。有两个比较关键的注解
- @runwith(springrunner.class):当一个类用@runwith注释或继承一个用@runwith注释的类时,junit将调用它所引用的类来运行该类中的测试而不是开发者去在junit内部去构建它,因此这句代码意思是让测试运行于spring测试环境中,springrunner仅仅继承了springjunit4classrunner而已,并没有扩展什么功能,前者可以看作是后者的“别名”。
- @springboottest:可以自动搜寻@springbootconfiguration;在没有明确指定@contextconfiguration(loader=...)时,使用springbootcontextloader作为默认的contextloader,等等。
package com.shy.springboot.service; import com.shy.springboot.domain.girl; import com.shy.springboot.repository.girlrepo; import org.junit.assert; import org.junit.test; import org.junit.runner.runwith; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.test.context.junit4.springrunner; /** * runwith(springrunner.class),让测试运行于spring测试环境 */ @runwith(springrunner.class) @springboottest public class girlservicetest { @autowired private girlrepo girlrepo; @test public void findone() { girl girl = girlrepo.findbyid(22).get(); assert.assertequals(new integer(14), girl.getage()); } }
然后针对controller层,对某次请求进行测试,这里使用到了mockmvc。
package com.shy.springboot.controller; import org.junit.test; import org.junit.runner.runwith; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.autoconfigure.web.servlet.autoconfiguremockmvc; import org.springframework.boot.test.context.springboottest; import org.springframework.test.context.junit4.springrunner; import org.springframework.test.web.servlet.mockmvc; import org.springframework.test.web.servlet.request.mockmvcrequestbuilders; import org.springframework.test.web.servlet.result.mockmvcresultmatchers; @runwith(springrunner.class) @springboottest @autoconfiguremockmvc public class girlcontrollertest { @autowired private mockmvc mockmvc; @test public void girls() throws exception { mockmvc.perform(mockmvcrequestbuilders.get("/girls")).andexpect(mockmvcresultmatchers.status().isok()); /* 下面这条测试不能通过 */ // mockmvc.perform(mockmvcrequestbuilders.get("/girls")).andexpect(mockmvcresultmatchers.content().string("abc")); } }
第一条测试模拟以get方法请求/girls
,并期望状态码是200 ok。注释掉的第二条测试期望响应的内容是abc,然而我们返回的是json格式,所以肯定不能通过测试的。
2018.10.4