一些技巧
SSM架构
image.png
表现层主要有controller作为外部接口访问和业务层service接口的连接;业务层处于中间层次,进行接口访问后的后续逻辑处理,并通过service调用持久层mybatis的mapper,而持久层的mapper具体的SQL操作在xml文件之中实现,最后操作mysql数据库。
image.png
SSM程序执行流程
yuque_diagram.jpg
业务逻辑:
Controller-->service接口-->serviceImpl-->mapper(dao)接口-->mapperImpl(daoImpl)-->mapper-->db
其中impl为模板代码,通过注解简化,在编译的时候自动生成注解实现类。
基本架构
application.yml 指定服务端口
server:
port: 5088
compression:
enabled: true
pom.xml基本配置
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.5.RELEASE
com.springboot
demo
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-maven-plugin
一、Controller 外部层
controller层即控制层。
controller层的功能为请求和响应控制。
controller层负责前后端交互,接受前端请求,调用service层,接收service层返回的数据,最后返回具体的页面和数据到客户端。
RestController注解
RequestMapping注解
@RequestMapping可以作用在类上,也可以作用在方法上。
@GetMapping其实就是@RequestMapping和Get的集合:
@GetMapping(value = “hello”) 等价于@RequestMapping(value = “hello”, method = RequestMethod.GET)
RequestParam注解
@RequestParam:将请求参数绑定到你控制器的方法参数上(是springmvc中接收普通参数的注解)
语法:@RequestParam(value=”参数名”,required=”true/false”,defaultValue=””)
value:参数名
required:是否包含该参数,默认为true,表示该请求路径中必须包含该参数,如果不包含就报错。
defaultValue:默认参数值,如果设置了该值,required=true将失效,自动为false,如果没有传该参数,就使用默认值
例子代码:
@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private UserService mUserService;
@RequestMapping(value = "/registration",method = RequestMethod.POST)
public Object registration(@RequestParam(value = "username") String username, @RequestParam(value = "password") String password, @RequestParam(value = "memberLevelId",defaultValue = "4") int memberLevelId, @RequestParam(value = "phone") String phone ){
System.out.printf("username:"+username+",password:"+password +",memberLevelID:"+memberLevelId+",phone:"+phone);
mUserService.addUser(username,password,memberLevelId,phone, DateUtil.currentDate());
return "registration success! ";
}
}
二、Service逻辑层
service层即业务逻辑层。
service层的作用为完成功能设计。
service层调用dao层接口,接收dao层返回的数据(封装为对象Model层),完成项目的基本功能设计。
存放业务逻辑处理,也是一些关于数据库处理的操作,但不是直接和数据库打交道,他有接口还有接口的实现方法,在接口的实现方法中需要导入mapper层,mapper层是直接跟数据库打交道的,他也是个接口,只有方法名字,具体实现在mapper.xml文件里,service是供我们使用的方法。
Repository注解
例子代码:
@Repository
public class UserService {
@Autowired
private UserMapper mUserMapper;
public void addUser(String username,String password,int memberLevelId,String phone,String createTime){
mUserMapper.addUser(username, password, memberLevelId, phone, createTime);
}
}
Autowired注解
SpringBoot @Autowired解析
autowired有4种模式,byName、byType、constructor、autodectect
@Autowired在何处使用
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
CONSTRUCTOR:构造
METHOD:方法
PARAMETER:参数
FIELD:字段
ANNOTATION_TYPE:注解
@Autowired参数
Autowired注解,只有一个required元素,默认是true,也是就是说这个值能改为false。true和false的意义不同。
require=ture 时,表示解析被标记的字段或方法,一定有对应的bean存在。
require=false 时,表示解析被标记的字段或方法,没有对应的bean存在不会报错。
public @interface Autowired {
/**
* Declares whether the annotated dependency is required.
*
Defaults to {@code true}.
*/
boolean required() default true;
}
三、Mapper层(Dao层)
不管是什么框架,我们很多时候都会与数据库进行交互。如果遇到一个场景我们都要去写SQL语句,那么我们的代码就会很冗余。所以,我们就想到了把数据库封装一下,让我们的数据库的交道看起来像和一个对象打交道,这个对象通常就是DAO。当我们操作这个对象的时候,这个对象会自动产生SQL语句来和数据库进行交互,我们就只需要使用DAO就行了。
我们经常说的DAO = Data Access Object = 数据存取对象。通常网络上我们说的Dao不仅包含了数据库的Model数据结构还包含了对应的数据操作方法,但是有一些却把它当成纯粹的model,所以更倾向于称为Mapper层。
通常我们在Mapper层里面写接口(Mapper层包括了数据对象既model),里面有与数据打交道的方法。SQL语句通常写在mapper文件里面的。
优点:结构清晰,Dao层的数据源配置以及相关的有关数据库连接的参数都在Spring配置文件中进行配置
总结:
mapper层即数据持久层,也被称为dao层。
mapper层的作用为访问数据库,向数据库发送sql语句,完成数据的增删改查任务。
现在用mybatis逆向工程生成的mapper层。对数据库进行数据持久化操作,他的方法语句是直接针对数据库操作的,而service层是针对我们controller,也就是针对我们使用者。service的impl是把mapper和service进行整合的文件。
集成MySQL
MySQL配置
pom.xml添加
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
runtime
在application.yml中添加:
spring:
#配置数据源
datasource:
url: jdbc:mysql://localhost:3306/mytest?characterEncoding=utf8&useSSL=true&serverTimezone=UTC
username: root #数据库用户名
password: root #数据库密码
mysql相关工具使用
集成MyBatis
mybatis配置
在application.yml中添加:
mybatis:
mapper-locations: classpath:mybatis/*.xml # 注意:一定要对应mapper映射xml文件的所在路径
type-aliases-package: com.springboot.demo.entity # 注意:对应实体类的路径
configuration:
map-underscore-to-camel-case: true # 数据库字段下划线自动转驼峰
在pom.xml依赖中添加
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.3
DemoApplication添加MapperScan注解
@MapperScan("com.springboot.demo")
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
UserMapper 接口添加数据库操作外部方法
@Repository
public interface UserMapper {
/**
* @param username 用户名
* @param password 密码
* @param memberLevelId 会员级别
* @param phone 手机号码
*/
void addUser(String username,String password,int memberLevelId,String phone,String createTime);
}
Mapper创建xml,实现底层SQL的执行操作
/p>
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
INSERT INTO t_user ( user_name, pwd, imooc_id, order_id, create_time )
VALUES
( #{userName}, #{password}, #{imoocId}, #{orderId}, #{createTime} )
ON DUPLICATE KEY UPDATE
pwd=VALUES(pwd)
,user_name=VALUES(user_name);
四、Model层
model层即数据库实体层,也被称为entity层,pojo层。存放我们的实体类,与数据库中的属性值基本保持一致。比如service层返回controller层的数据,封装为对象model,更加符合面向对象编程的习惯。
乱码错误
之前建立项目是utf8,但是不知道怎么操作的application.yml有一次变成乱码,于是用GBK重载,注释的中文恢复了,但是执行代码发现报错
Caused by: org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input l
其实应该是文件的格式问题造成的解析错误,改变改文件的解析格式为utf8,或者新建一个application.yml文件覆盖,里面乱码的中文,重新输入即可。
对于PostMan的使用教程,可以查看postman使用教程
image.png
报错,500, "error": "Internal Server Error",,查看服务端log,create_time字段出问题。
image.png
UserService的addUser 调用UserMapper
image.png
而UserMapper在UserMapper.xml使用了create_time对应不上UserMapper.java的createTime报错,将UserMapper.xml的create_time改为createTime即可
image.png
重新请求postman
image.png
更改请求时的密码,发现并没有新注册一个账户,而是直接update数据库,证明我们的mapper的SQL语句是正确的
自定义配置
@Configuration底层是含有@Component ,所以@Configuration 具有和 @Component 的作用。
@Configuration可理解为用spring的时候xml里面的标签。
@Configuration注解可以达到在Spring中使用xml配置文件的作用。
@Bean可理解为用spring的时候xml里面的标签
@Configuration注解
@Bean注解
BCrypt实现用户信息加密
密码的存储,传输使用明文是比较不安全的做法,所以我们通过集成BCrypt实现用户信息加密。
pom.xml添加配置
org.springframework.boot
spring-boot-starter-security
添加SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//定义所有要控制的路径
.antMatchers("/**")
//允许所有人访问上述路径
.permitAll()
.and().csrf().disable();
super.configure(http);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
image.png
自定义配置类入口WebSecurityConfigurerAdapter
HttpSecurity 使用了builder 的构建方式来灵活制定访问策略。最早基于 XML 标签对 HttpSecurity 进行配置。现在大部分使用 javaConfig方式。常用的方法解读如下:
方法
说明
openidLogin()
用于基于 OpenId 的验证
headers()
将安全标头添加到响应,比如说简单的 XSS 保护
cors()
配置跨域资源共享( CORS )
sessionManagement()
允许配置会话管理
portMapper()
允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee()
配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509()
配置基于x509的认证
rememberMe
允许配置“记住我”的验证
authorizeRequests()
允许基于使用HttpServletRequest限制访问
requestCache()
允许配置请求缓存
exceptionHandling()
允许配置错误处理
securityContext()
在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi()
将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf()
添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout()
添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”
anonymous()
允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin()
指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login()
根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel()
配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic()
配置 Http Basic 验证
addFilterBefore()
在指定的Filter类之前添加过滤器
addFilterAt()
在指定的Filter类的位置添加过滤器
addFilterAfter()
在指定的Filter类的之后添加过滤器
and()
连接以上策略的连接器,用来组合安全策略。实际上就是"而且"的意思
我们的项目
authorizeRequests():允许基于使用HttpServletRequest限制访问
antMatchers("/**"):定义所有要控制的路径
/* 是拦截所有的文件夹,不包含子文件夹
/** 是拦截所有的文件夹及里面的子文件夹
也能够对不同路径设置不同的配置
.antMatchers("/resources/**", "/signup", "/about").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
antMatchers("/**").permitAll() 表示项目根目录以及子目录全部都 permitAll 允许所有人访问上述路径
在项目中添加了Security模块后,在发送post请求时会失败,出现以下日志:
Invalid CSRF token found for ..
在Security的默认拦截器里,默认会开启CSRF处理,判断请求是否携带了token,如果没有就拒绝访问。并且,在请求为(GET|HEAD|TRACE|OPTIONS)时,则不会开启,可参考如下源码:
// 先从tokenRepository中加载token
CsrfToken csrfToken = tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
// 如果为空,则tokenRepository生成新的token,并保存到tokenRepository中
if(missingToken) {
CsrfToken generatedToken = tokenRepository.generateToken(request);
// 默认的SaveOnAccessCsrfToken方法,记录tokenRepository,
// tokenRepository,response,获取token时先将token同步保存到tokenRepository中
csrfToken = new SaveOnAccessCsrfToken(tokenRepository, request, response, generatedToken);
}
// 将token写入request的attribute中,方便页面上使用
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
// 如果不需要csrf验证的请求,则直接下传请求(requireCsrfProtectionMatcher是默认的对象,对符合^(GET|HEAD|TRACE|OPTIONS)$的请求不验证)
if(!requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
// 从用户请求中获取token信息
String actualToken = request.getHeader(csrfToken.getHeaderName());
if(actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
// 验证,如果相同,则下传请求,如果不同,则抛出异常
if(!csrfToken.getToken().equals(actualToken)) {
if(logger.isDebugEnabled()) {
logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
}
if(missingToken) {
accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken));
} else {
accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
filterChain.doFilter(request, response);
所以 csrf().disable()关掉csrf
整合Swagger文档
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
application
@MapperScan("com.springboot.demo")
@SpringBootApplication
@EnableSwagger2
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
添加配置类
添加一个swagger 配置类,在工程下新建 config 包并添加一个 SwaggerConfig 配置类。
@Configuration
@EnableSwagger2
public class SwaggerConfig {
public static final String VERSION ="1.0.1";
public static final String AUTHOR="spring boot demo";
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com\\springboot\\demo\\controller"))
.paths(PathSelectors.any())
.build()
.ignoredParameterTypes(ApiIgnore.class);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("API 文档")
.description("SpringBoot demo API 文档")
.version(VERSION)
.contact(new Contact(AUTHOR,"https://www.baidu.com","XXXX@gmail.com"))
.build();
}
}
Swagger 注解
@Api:用在请求的类上,表示对类的说明
tags="说明该类的作用,可以在UI界面上看到的注解"
value="该参数没什么意义,在UI界面上也看到,所以不需要配置"
@ApiOperation:用在请求的方法上,说明方法的用途、作用
value="说明方法的用途、作用"
notes="方法的备注说明"
@ApiImplicitParams:用在请求的方法上,表示一组参数说明
@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面
name:参数名
value:参数的汉字说明、解释
required:参数是否必须传
paramType:参数放在哪个地方
· header --> 请求参数的获取:@RequestHeader
· query --> 请求参数的获取:@RequestParam
· path(用于restful接口)--> 请求参数的获取:@PathVariable
· body(不常用)
· form(不常用)
dataType:参数类型,默认String,其它值dataType="Integer"
defaultValue:参数的默认值
@ApiResponses:用在请求的方法上,表示一组响应
@ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息
code:数字,例如400
message:信息,例如"请求参数没填好"
response:抛出异常的类
@ApiModel:用于响应类上,表示一个返回响应数据的信息
(这种一般用在post创建的时候,使用@RequestBody这样的场景,
请求参数无法使用@ApiImplicitParam注解进行描述的时候)
@ApiModelProperty:用在属性上,描述响应类的属性
示例
@Api:用在请求的类上,说明该类的作用
@RestController
@RequestMapping(value = "/user")
@Api(tags = {"Member"})
public class UserController {
@ApiOperation(value = "登录")
@ApiParam("登录账号")
@ApiOperation(value = "注册")
@RequestMapping(value = "/registration",method = RequestMethod.POST)
public ResponseEntity registration(@RequestParam(value = "username") @ApiParam("用户名") String username,
@RequestParam(value = "password") @ApiParam("密码") String password,
@RequestParam(value = "memberLevelId",defaultValue = "4") @ApiParam("会员等级") int memberLevelId,
@RequestParam(value = "phone") @ApiParam("手机号码") String phone ){
// System.out.printf("username:"+username+",password:"+password +",memberLevelID:"+memberLevelId+",phone:"+phone);
mUserService.addUser(username, bCryptPasswordEncoder.encode(password),memberLevelId,phone, DateUtil.currentDate());
return ResponseEntity.successMessage("registration success! ");
}
@ApiOperation(value="用户注册",notes="手机号、密码都是必输项,年龄随边填,但必须是数字")
@ApiImplicitParams:用在请求的方法上,包含一组参数说明
@ApiImplicitParams({
@ApiImplicitParam(name="mobile",value="手机号",required=true,paramType="form"),
@ApiImplicitParam(name="password",value="密码",required=true,paramType="form"),
@ApiImplicitParam(name="age",value="年龄",required=true,paramType="form",dataType="Integer")
})
@ApiResponses:用于请求的方法上,表示一组响应
@ApiOperation(value = "select1请求",notes = "多个参数,多种的查询参数类型")
@ApiResponses({
@ApiResponse(code=400,message="请求参数没填好"),
@ApiResponse(code=404,message="请求路径没有或页面跳转路径不对")
})
@ApiModel:用于响应类上,表示一个返回响应数据的信息
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
@ApiModel(description= "返回响应数据")
public class RestMessage implements Serializable{
@ApiModelProperty(value = "是否成功")
private boolean success=true;
@ApiModelProperty(value = "返回对象")
private Object data;
@ApiModelProperty(value = "错误编号")
private Integer errCode;
@ApiModelProperty(value = "错误信息")
private String message;
/* getter/setter */
}
访问swagger接口文档界面
image.png
测试接口
展开member的/user/login登录接口,点击Try it out,就可以输入参数并点击执行。
image.png
输入参数并点击执行
image.png
看到返回信息,提示密码错误,程序正常
image.png
整合 Redis
pom.xml 集成服务
org.springframework.boot
spring-boot-starter-data-redis
Redis 工具类
public class UserRedisUtil {
public static String BOARDING_PASS = "boarding-pass";
/**
* 将用户信息保存到redis
* @param redisTemplate
* @param session
* @param userEntity
*/
public static void addUser(StringRedisTemplate redisTemplate, HttpSession session, UserEntity userEntity){
//用户session写入redis
redisTemplate.opsForValue().set(getKey(session),JsonUtil.toJsonString(userEntity));
}
/**
* 将用户信息从redis中移除
* @param redisTemplate
* @param session
*/
public static void removeUser(StringRedisTemplate redisTemplate,HttpSession session){
session.invalidate();
//将用户从redis中移除
redisTemplate.delete(getKey(session));
}
public static UserEntity getUser(StringRedisTemplate redisTemplate, HttpServletRequest request){
//检测redis是否含有session
UserEntity userEntity = null;
String userJson = redisTemplate.opsForValue().get(getBoardingPass(request));
if(userJson != null){
userEntity = JsonUtil.fromJson(userJson,UserEntity.class);
}
return userEntity;
}
/**
* 获取redis存储的key
* @param session
* @return
*/
public static String getKey(HttpSession session){
return session.getId();
}
public static String getBoardingPass(HttpServletRequest request){
String pass =request.getHeader(BOARDING_PASS);
return pass != null ?pass : "no-pass";
}
}