基础
关于http协议
HTTP特点
-
http协议支持客户端/服务端模式,也是一种请求/响应模式的协议。
-
简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。
-
灵活:HTTP允许传输任意类型的数据对象。传输的类型由Content-Type加以标记。
-
无连接:限制每次连接只处理一个请求。服务器处理完请求,并收到客户的应答后,即断开连接,但是却不利于客户端与服务器保持会话连接,为了弥补这种不足,产生了两项记录http状态的技术,一个叫做Cookie,一个叫做Session。
-
无状态:无状态是指协议对于事务处理没有记忆,后续处理需要前面的信息,则必须重传。
响应状态码
访问一个网页时,浏览器会向web服务器发出请求。此网页所在的服务器会返回一个包含HTTP状态码的信息头用以响应浏览器的请求。
关于content-type
Text:用于标准化地表示的文本信息,文本消息可以是多种字符集和或者多种格式的;
Multipart:用于连接消息体的多个部分构成一个消息,这些部分可以是不同类型的数据;
Application:用于传输应用程序数据或者二进制数据;
Message:用于包装一个E-mail消息;
Image:用于传输静态图片数据;
Audio:用于传输音频或者音声数据;
Video:用于传输动态影像数据,可以是与音频编辑在一起的视频数据格式。
状态码分类:
- 1XX- 信息型,服务器收到请求,需要请求者继续操作。
- 2XX- 成功型,请求成功收到,理解并处理。
- 3XX - 重定向,需要进一步的操作以完成请求。
- 4XX - 客户端错误,请求包含语法错误或无法完成请求。
- 5XX - 服务器错误,服务器在处理请求的过程中发生了错误。
关于HttpServletRequest request
request.getRequestURL(): 浏览器发出请求时的完整URL,包括协议 主机名 端口(如果有)" request.getRequestURI(): 浏览器发出请求的资源名部分,去掉了协议和主机名" request.getQueryString(): 请求行中的参数部分,只能显示以get方式发出的参数,post方式的看不到 request.getRemoteAddr(): 浏览器所处于的客户机的IP地址 request.getRemoteHost(): 浏览器所处于的客户机的主机名 request.getRemotePort(): 浏览器所处于的客户机使用的网络端口 request.getLocalAddr(): 服务器的IP地址 request.getLocalName(): 服务器的主机名 request.getMethod(): 得到客户机请求方式一般是GET或者POST
request.getParameter(): 是常见的方法,用于获取单值的参数 request.getParameterValues(): 用于获取具有多值的参数,比如注册时候提交的 "hobits",可以是多选的。 request.getParameterMap(): 用于遍历所有的参数,并返回Map类型。
关于HttpServletResponse response
addHeader(String name, String value) setHeader(String name, String value) 这两个方法都是用来设置HTTP协议的响应头字段,其中,参数name用于指定响应头字段的名称,参数value用于指定响应头字段的值。不同的是,addHeader()方法可以增加同名的响应头字段,而setHeader()方法则会覆盖同名的头字段 addIntHeader(String name,int value) setIntHeader(String name,int value) 这两个方法专门用于设置包含整数值的响应头。避免了使用addHeader()与setHeader()方法时,需要将int类型的设置值转换为String类型的麻烦 setContentLength(int len) 该方法用于设置响应消息的实体内容的大小,单位为字节。对于HTTP协议来说,这个方法就是设置Content-Length响应头字段的值 setContentType(String type) 该方法用于设置Servlet输出内容的MIME类型,对于HTTP协议来说,就是设置Content-Type响应头字段的值。例如,如果发送到客户端的内容是jpeg格式的图像数据,就需要将响应头字段的类型设置为“image/jpeg”。需要注意的是,如果响应的内容为文本,setContentType()方法的还可以设置字符编码,如:text/html;charset=UTF-8 setCharacterEncoding(String charset) 该方法用于设置输出内容使用的字符编码,对HTTP 协议来说,就是设置Content-Type头字段中的字符集编码部分。一般不使用。 getOutputStream() 该方法所获取的字节输出流对象为ServletOutputStream类型。由于ServletOutputStream是OutputStream的子类,它可以直接输出字节数组中的二进制数据。因此,要想输出二进制格式的响应正文,就需要使用getOutputStream()方法。 getWriter() 该方法所获取的字符输出流对象为PrintWriter类型。由于PrintWriter类型的对象可以直接输出字符文本内容,因此,要想输出内容全为字符文本的网页文档,需要使用getWriter()方法 tomcat使用getWriter()处理字符时,默认编码时ISO8859-1。响应给浏览器的数据,此时为乱码, 解决办法 setStatus(int status) 该方法用于设置HTTP响应消息的状态码,并生成响应状态行。由于响应状态行中的状态描述信息直接与状态码相关,而HTTP版本由服务器确定,因此,只要通过setStatus(int status)方法设置了状态码,即可实现状态行的发送。需要注意的是,正常情况下,Web服务器会默认产生一个状态码为200的状态行。 sendError(int sc) 该方法用于发送表示错误信息的状态码,例如,404状态码表示找不到客户端请求的资源。
1Web容器配置
1常规配置
一个springboot项目往往内置tomcat,hetty,undertow,netty等容器
当添加spring-boot-start-web依赖以后默认使用tomcat
//端口号配置
server.port=8080
//当前项目出错跳转页面
server.error.path=
//session失效时间,默认单位为秒,如果为秒则向下取整转化为分钟,m为分钟
server.servlet.seesion.timeout=30m
//项目名称,不配置默认为/。如果配置了,就要在访问路径中加上配置的路径
server.servlet.context-path=
//表示配置tomcat请求编码
server.tomcat.uri-encoding=utf-8
//表示tomcat最大线程数
server.tomcat.max-threads=500
//存放tomcat运行日记和临时文件的目录,不配置,则使用默认路径
server.tomcat.basedir=
更多配置参考官方文档 application properties一节
2https配置
可以使用云服务器商提供的免费https证书
也可以使用\jdk\bin生产的数字证书
//在\jdk\bin下
keytool -genkey -alias tomcathttps -keyalg RSA -keysize 2048 -keystore sang.p12
-validity 365
-genkey 生成一个密钥
-alias keystore的别名
-keyalg 选择加密算法
-keysize 密钥长度
-keystore 密钥存放位置
-validity 有效期 单位天
//密钥文件名
server-ssl.key-store=song.p12
//密钥别名
server-ssl.key-alias=tomcathttps
//cmd命令中输入的密码
server.ssl.key-store-password =
因为springboot不支持同时启动http和https,所以可以配置请求重定向
@Bean
public EmbeddedServletContainerFactory servletContainer() {
TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};
factory.addAdditionalTomcatConnectors(httpConnector());
return tomcat;
}
@Bean
public Connector httpConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
//Connector监听的http的端口号
connector.setPort(8080);
connector.setSecure(false);
//监听到http的端口号后转向到的https的端口号
connector.setRedirectPort(8443);
return connector;
}
2Properties配置
1配置位置
其一共可以出现在四个位置,读取优先级由高到低为
*根目录的config中
*根目录
*classpath的config中(src文件夹下的)
*classpath下
yml也与其一致
2类型安全配置属性
自定义
citycode.properties
#List properties
citycode.list[0]=www
citycode.list[1]=localhost
citycode.list[2]=wuhan
citycode.list[3]=tianjin
#Map Properties
citycode.map.www=4201
citycode.map.wuhan=4201
citycode.map.tianjin=1200
citycode.name=dadad
city.author=asdasd
@Configuration
@PropertySource("classpath:citycode.properties")
//prefix是配置文键的前缀
@ConfigurationProperties(prefix = "citycode")
public class CityCodeConfig {
private List<String> list = new ArrayList<>();
private Map<String, String> map = new HashMap<>();
private String name;
private String author;
}
//这样就可以了
@RestController
public class test{
//注入
@AutoWired
CityCodeConfig c;
@GetMapping("/test")
public String fun()
{
return c.toString;
}
}
另外如果bean的属性为authorName,那么配置文件的属性可以是book.author_name,book.author-book,book.AUTHORNAME
关于@value
- 一、 @Value(“#{}”) 1 @Value(“#{}”) SpEL表达式(blog.csdn.net/ya_12494633… @Value(“#{}”) 表示SpEl表达式通常用来获取bean的属性,或者调用bean的某个方法。当然还有可以表示常量
@RestController
@RequestMapping("/login")
@Component
public class LoginController {
@Value("#{1}")
private int number; //获取数字 1
@Value("#{'Spring Expression Language'}") //获取字符串常量
private String str;
@Value("#{dataSource.url}") //获取bean的属性
private String jdbcUrl;
@Autowired
private DataSourceTransactionManager transactionManager;
@RequestMapping("login")
public String login(String name,String password) throws FileNotFoundException{
System.out.println(number);
System.out.println(str);
System.out.println(jdbcUrl);
return "login";
}
}
二、 @Value(“${}”)
blog.csdn.net/zengdeqing2… blog.csdn.net/jiangyu1013… 1.用法 从配置properties文件中读取init.password 的值。
@Value("${init.password}")
private String initPwd;
关于@Autowired和@Resource
相同:
@Resource和@Autowired都可以作为注入属性的修饰,在接口仅有单一实现类时,两个注解的修饰效果相同,可以互相替换,不影响使用。
不同:
@Resource是Java自己的注解,@Resource有两个属性是比较重要的,分是name和type;Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略,这个不能用于构造器。 @Autowired是spring的注解,是spring2.5版本引入的,Autowired只根据type进行注入,不会去匹配name。如果涉及到type无法辨别注入对象时,那需要依赖@Qualifier或@Primary注解一起来修饰。
什么意思呢?
假设我定义一个接口
public interface Human {
String runMarathon();
}
@Service
public class Man implements Human {
public String runMarathon() {
return "A man run marathon";
}
}
@RestController
@RequestMapping("/an")
public class HumanController {
//此时@Autowired与@Resource一致
//此时注入的是man
@Resource
private Human human;
@RequestMapping("/run")
public String runMarathon() {
return human.runMarathon();
}
}
如果还有实现类
@Service
public class Woman implements Human {
public String runMarathon() {
return "An woman run marathon";
}
}
这两注解都会报错,报错信息是提示有多个适合结构可以注入
这里,我们需要借助@Resource注解的name属性或@Qualifier来确定一个合格的实现类
@Resource(name="woman")
private Human human;
或
@Resource
@Qualifier("woman")
private Human human;
此时换成@Autowired则会报错
可以修改实现类为
@Qualifier修改方式于上文一致
或者
@Primary是修饰实现类的,告诉spring,如果有多个实现类时,优先注入被@Primary注解修饰的那个。
@Service
@Primary
public class Man implements Human {
public String runMarathon() {
return "A man run marathon";
}
}
3整合视图
前后端分离,后端写个锤子的界面
4Web开发基础
1json数据返回
1默认json处理器
starter依赖默认存在一个json处理器
@JsonIgnore注解在属性上,表明不json化这个属性
@JsonFormat(pattern=“YYYY-MM-DD”)格式化一个日期
@Response返回值处理为一个json,在参数上就是意味着这是一个json传来的数据
@RestController是@Response和@Controller的组合注解
对于json数据的处理我个人推荐阿里的fastjson,关于这个的说明点击这里
2集成fastjson
先去除jsackson-databind依赖,导入fastjson依赖https://mvnrepository.com/artifact/com.alibaba/fastjson
@Configuration
public class MyFastJsonConfig {
@Bean
FastJsonHttpMessageConverter fastJsonHttpMessageConverter(){
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config=new FastJsonConfig();
config.setDateFormat("yyyy-mm-dd");
config.setCharset(Charset.forName("UTF-8"));
config.setSerializerFeatures(
SerializerFeature.WriteClassName,
SerializerFeature.WriteMapNullValue,
SerializerFeature.PrettyFormat,
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.WriteNullStringAsEmpty
);
converter.setFastJsonConfig(config);
return converter;
}
}
一定不要起名字为FastJsonConfig,一定,因为和他本身的那个重了,我又不想写全名
2静态资源的访问
默认
按照定义的顺序,静态资源可存在五个位置,按优先级从高到低排列
*classpath:/META-INF/resources/
*classpath:/resources/
*classpath:/static/(如果使用idae创建项目,则会默认创建这个,一般直接放在这个下面)
*classpath:/public/
*classpath:/
对于静态资源的自定义策略
配置文件
spring.mbc.static-path-pattern=/static/**
spring.resources.static-location=classpath:/static
过滤规则为/static/**
静态资源位置为classpath:/static/
java类定义
@Configuration
public class MyStaticResourcesConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
}
3文件上传与下载
其实所有传输文件的原理都和http报文有关 举个例子 httpServletResponse.setContentType("image/png");
常见的媒体格式类型如下:
text/html : HTML格式
text/plain :纯文本格式
text/xml : XML格式
image/gif :gif图片格式
image/jpeg :jpg图片格式
image/png:png图片格式
以application开头的媒体格式类型:
application/xhtml+xml :XHTML格式
application/xml: XML数据格式
application/atom+xml :Atom XML聚合格式
application/json: JSON数据格式
当我们使用@Responsebody注解时,返回值为就是自动转换为string字符串(符合json格式的字符串)然后写入响应体
application/pdf:pdf格式
application/msword : Word文档格式
application/octet-stream : 二进制流数据(如常见的文件下载)
application/x-www-form-urlencoded :
中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
另外一种常见的媒体格式是上传文件之时使用的: multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式
工具类
public enum ContentTypeUtil {
HTML("text/html;charset=UTF-8"),
PLAIN("text/plain;charset=UTF-8"),
MP3("audio/mp3"),
MP4("audio/mp4"),
XML_TEXT("text/xml"),
GIF("image/gif"),
JPEG("image/jpeg"),
PNG("image/png"),
XHTML("application/xhtml+xml"),
XML_APPLICATION("application/xml"),
ATOMXML("application/atom+xml"),
JSON("application/json"),
PDF("application/pdf"),
WORD("application/msword"),
//二进制数据流,文件下载
DOWLOAD("application/octet-stream"),
X_WWW_FORM_URLENCODE("application/x-www-form-urlencoded"),
MULTIPART_FORM_DATA("multipart/form-data");
String contentType;
private ContentTypeUtil(String contentType)
{
this.contentType=contentType;
}
public String getContentType()
{
return contentType;
}
}
如果开发者没有提供MultipartResovler,那么默认采用的是MultipartResovler就是StandardServletMultipartResovler,因此springboot文件上传可以零配置
对图片上传的细节配置
//是否支持 multipart 上传文件,默认true
spring.servlet.multipart.enabled=true
//文件写入磁盘的阈值,默认为0
spring.servlet.multipart.file-size-threshold=0
//上传文件的临时目录
spring.servlet.multipart.location=
//最大大支持文件大小,默认为1mb
spring.servlet.multipart.max-file-size=10Mb
//多文件上传时的总大小。默认为10mb
spring.servlet.multipart.max-request-size=10Mb
//是否支持 multipart 上传文件时懒加载,是否延迟解析,默认false
spring.servlet.multipart.resolve-lazily=false
1单文件上传
文件路径可以设为System.getProperty("user.dir")
//C:\Users\10756\Desktop\my\express
先建立一个upload.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="uploadFile" value="请选择文件" multiple>
<input type="submit" value="上传">
</form>
</body>
</html>
对应控制器
@PostMapping("/upload")
public String upload(MultipartFile uploadFile, HttpServletRequest req) {
//保存
String realPath = req.getSession().getServletContext().getRealPath("/uploadFile/");
System.out.println(realPath);
String format = sdf.format(new Date());
File folder = new File(realPath + format);
if (!folder.isDirectory()) {
folder.mkdirs();
}
//重命名
String oldName = uploadFile.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."), oldName.length());
try {
//保存
uploadFile.transferTo(new File(folder, newName));
//返回路径
String filePath = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/uploadFile/" + format + newName;
return filePath;
} catch (IOException e) {
// e.printStackTrace();
}
return "上传失败!";
}
2多文件上传
对应html就是改了个接口
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/uploads" method="post" enctype="multipart/form-data">
<input type="file" name="uploadFile" value="请选择文件" multiple>
<input type="submit" value="上传">
</form>
</body>
</html>
对应控制器,多了一个遍历的过程
@PostMapping("/uploads")
public String upload(MultipartFile[] uploadFiles, HttpServletRequest req) {
for (MultipartFile uploadFile : uploadFiles) {
String realPath = req.getSession().getServletContext().getRealPath("/uploadFile/");
System.out.println(realPath);
String format = sdf.format(new Date());
File folder = new File(realPath + format);
if (!folder.isDirectory()) {
folder.mkdirs();
}
String oldName = uploadFile.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."), oldName.length());
try {
uploadFile.transferTo(new File(folder, newName));
String filePath = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/uploadFile/" + format + newName;
// return filePath;
System.out.println(filePath);
} catch (IOException e) {
e.printStackTrace();
}
}
return "上传失败!";
}
3文件下载
@RequestMapping("/test/download")
public void test3(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
{
httpServletResponse.setContentType("application/octet-stream");
File file = new File("C:\\Users\\10756\\Desktop\\test.doc");
try {
FileInputStream fileInputStream = new FileInputStream(file);
//如果filename带有中文应该使用URLEncoder.encode(file.getFilename(), "utf-8")
//否则会导致名称变为下划线
httpServletResponse.setHeader("Content-Disposition", "attachment; filename="+UUID.randomUUID().toString());
IOUtils.copy(fileInputStream, httpServletResponse.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
4向前端传输一个照片
@RequestMapping("/test/jpg")
public void test2(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse)
{
httpServletResponse.setContentType("image/png");
try {
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\10756\\Desktop\\test.png");
//别忘记关闭流
IOUtils.copy(fileInputStream, httpServletResponse.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
4@ControllerAdvice(待续)
@ControllerAdvice主要用来处理全局数据,一般@ExceptionHandler,@ModelAttribute以及@InitBinder使用
1@ExceptionHandler
前提是Controller层没有对异常进行catch处理,如果Controller层对异常进行了catch处理,那么在这里就不会捕获到Controller层的异常
返回值可以是json,也可以时ModelAndView,一个逻辑视图名
@ControllerAdvice
public class UploadException {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public String uploadExceptionHandle(MaxUploadSizeExceededException e){
return new JSONObject().fluentPut("error", "超过限制")
.toJSONString();
}
}
5拦截器配置
public class AppointmentInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getRequestURI().contains("/Appointment/counselor")) {
LoginUser loginUser = (LoginUser) request.getSession().getAttribute("login_user");
if (loginUser.getRole().equals("counselor")) {
return true;
}
else {
response.getWriter().print(new JSONObject(true).fluentPut("isSuccess","no")
.fluentPut("error","permission denied" ).toJSONString());
response.getWriter().flush();
response.getWriter().close();
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
注册
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/User/normal/login","/MessageBroad/normal/showAllMessage","/User/normal/register","/User/normal/isExist",
"/User/test");
registry.addInterceptor(new AppointmentInterceptor()).addPathPatterns("/Appointment/**");
registry.addInterceptor(new MessageBroadInterceptor()).addPathPatterns("/MessageBroad/**");
registry.addInterceptor(new UserInterceptor()).addPathPatterns("/User/**");
}
}
6 controller
RequestMapping注解有六个属性,下面我们把她分成三类进行说明。 1、 value, method;
value:指定请求的实际地址,指定的地址可以是URI Template 模式;
method: 指定请求的method类型, GET、POST、PUT、DELETE等(RequestMethod.*);
还有一个注意的,@RequestMapping的默认属性为value,所以@RequestMapping(value="/example")和@RequestMapping("/example")是等价的
//value的uri值为以下三类:
//A) 可以指定为普通的具体值;
//B) 可以指定为含有某变量的一类值(URI Template Patterns with Path Variables);
//C) 可以指定为含正则表达式的一类值( URI Template Patterns with Regular Expressions);
//example B)
@RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable String ownerId, Model model) {
Owner owner = ownerService.findOwner(ownerId);
model.addAttribute("owner", owner);
return "displayOwner";
}
//example C)
@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\d\.\d\.\d}.{extension:\.[a-z]}")
public void handle(@PathVariable String version, @PathVariable String extension) {
// ...
}
}
注解在类上则该类url路径前都要有对应value值
@RestController和@Controller区别在于前一个返回值写入到输出流中json
下面这个例子 访问这个方法就是…/controller/record
@RestController
@RequestMapping("/controller")
public class Controller {
@GetMapping("/record")
public ResponseInfo get(){
return new ResponseInfo();
}
}
2、 consumes,produces;
consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
@RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;
方法仅处理request请求中Accept头中包含了"application/json"的请求,同时暗示了返回的内容类型为application/json;
3、 params,headers;
params: 指定request中必须包含某些参数值是,才让该方法处理。
headers: 指定request中必须包含某些指定的header值,才能让该方法处理请求。
@RequestMapping(value = "/pets", method = RequestMethod.GET, headers="Referer=http://www.ifeng.com/")
5变量注解那些事
url相关
@PathVariable 绑定URI模板变量值 @PathVariable是用来获得请求url中的动态参数的 @PathVariable用于将请求URL中的模板变量映射到功能处理方法的参数上。//配置url和方法的一个关系@RequestMapping("item/{itemId}")
@RequestParam GET和POST请求传的参数会自动转换赋值到@RequestParam 所注解的变量上,以下面实例讲解
<form action="/WxProgram/json/requestParamTest" method="get">
requestParam Test<br>
用户名:<input type="text" name="username"><br>
用户昵称:<input type="text" name="usernick"><br>
<input type="submit" value="提交">
</form>
@RequestMapping(value="/requestParamTest", method = RequestMethod.GET)
//对于@RequestParam注解来讲,等价于HttpServletRequest.getParameter("usernick")
//如果不适用注解就要名称保持一致
@RequestParam(value="start",defaultValue ="0")可以指定一个默认值
public String requestParamTest(@RequestParam(value="username") String userName, @RequestParam(value="usernick") String userNick){
return "hello";
}
@RequestBody 注解可以接收json格式的数据,并将其转换成对应的数据类型。
//只要能一一对应就能之间组装为一个类,由于使用 @Restcontroller注解返回值也就是json
public Map<String, Object> getPerson(@RequestBody Person person) {
Map<String, Object> param = new HashMap<>();
String s = person.getPhones().toString();
System.out.println(s);
param.put("person", person);
return param;
}
- application/x-www-form-urlencoded,这种情况的数据@RequestParam、@ModelAttribute可以处理,@RequestBody也可以处理。
- multipart/form-data,@RequestBody不能处理这种格式的数据。(form表单里面有文件上传时,必须要指定enctype属性值为multipart/form-data,意思是以二进制流的形式传输文件。)
- application/json、application/xml等格式的数据,必须使用@RequestBody来处理。
参数校验
校验条件
public class ChatRecord implements Serializable {
private Long chatId;
@NotBlank(message = "内容不能为空")
private String content;
private java.sql.Timestamp timeStamp;
@NotBlank(message = "用户名不能为空")
private String customerName;
}
校验
public ResponseInfo addRecord(@Validated @RequestBody ChatRecord record,
BindingResult bindingResult){
if (bindingResult.hasErrors()) {
List<String> list = bindingResult.getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList());
return ResponseInfo.getFailure("failure", new HashMap<>(), list);
}
}
嵌套就是指集合
@Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
@Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
6Spring Boot安全管理
本节主要介绍Spring Security的事项
1基本用法
依赖管理:
mvnrepository.com/artifact/or…
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
集成Spring Security后默认情况下,无论访问什么接口都会跳转至自带的登录界面(指Spring Security自带),默认用户名为user,默认密码在控制台和日志打印输出为:
Using generated security password: 0332e7ba-ea02-451c-9556-4e516153fc37
也可以在application.properties中自己配置,这样就不会生成随机的密码和用户名了
spring.security.user.name=sun
spring.security.user.password=chenxi
spring.security.user.roles=admin
2基于内存的认证(以下均是)
jin00001
配置了两个用户,并且使用不加密的策略(5.*版本必须指定一个策略)
基于内存的配置角色不需要加入“ROLE_”前缀
roles方法支持一个变长String参数
@Configuration
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("123").roles("admin")
.and()
.withUser("su").password("123").roles("admin");
}
}
1配置认证功能,角色管理
基础:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//以下是配置对应url模式需要的权限
.antMatchers("/admin/**")
.hasRole("admin")
.antMatchers("/user/**")
.access("hasAnyRole('admin','user')")
.antMatchers("/db/**")
.access("hasRole('admin') and hasRole('dba')")
//下面是表明除了前面定义的url模式以外,访问其他url必须登陆后访问
.anyRequest()
.authenticated()
.and()
//开启表单登录,登录接口为/login,使用post请求,参数的用户名必须为username,密码必须为 password,使用loginProcessingUrl,主要方便Ajax或者移动端调用登录接口
.formLogin()
.loginProcessingUrl("/login").permitAll()
//permitAll,表明登录接口不要认证就能访问
.and()
//CSRF(Cross-site request forgery)跨站请求伪造关闭,默认启动
.csrf()
.disable();
}
什么是csrf
客户端与服务端在基于http协议在交互的数据的时候,由于http协议本身是无状态协议,后来引进了cookie的 方式进行记录服务端和客户端的之间交互的状态和标记。cookie里面一般会放置服务端生成的session id(会话ID)用来识别客户端访问服务端过 程中的客户端的身份标记。
再科普一下"跨域"
同一个ip、同一个网络协议、同一个端口,三者都满足就是同一个域,否则就有跨域问题 ,在跨域 的情况下 session id可能会被恶意第三方劫持,此时劫持这个session id的第三方会根据这个session id向服务器发起请求,此时服务器收到这个请求会 认为这是合法的请求,并返回根据请求完成相应的服务端更新。
spring security 中的几个关键点
1)如果这个http请求是通过get方式发起的请求,意味着它只是访问服务器 的资源,仅仅只是查询,没有更新服务器的资源,所以对于这类请求,spring security的防御策略是允许的;
2)如果这个http请求是通过post请求发起的, 那么spring security是默认拦截这类请求的,因为这类请求是带有更新服务器资源的危险操作,如果恶意第三方可以通过劫持session id来更新 服务器资源,那会造成服务器数据被非法的篡改,所以这类请求是会被Spring security拦截的,在默认的情况下,spring security是启用csrf 拦截功能的,这会造成,在跨域的情况下,post方式提交的请求都会被拦截无法被处理(包括合理的post请求),前端发起的post请求后端无法正常 处理,虽然保证了跨域的安全性,但影响了正常的使用,如果关闭csrf防护功能,虽然可以正常处理post请求,但是无法防范通过劫持session id的非法的post请求,所以spring security为了正确的区别合法的post请求,采用了token的机制 。
前后端分离登录
对于前后端分离的项目,登陆成功不再是页面跳转,而是通过json进行通讯
http://localhost:8080/User/normal/login?password=123&name=su(访问这个url)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("admin")
.antMatchers("/user/**")
.access("hasAnyRole('admin','user')")
.antMatchers("/db/**")
.access("hasRole('admin') and hasRole('dba')")
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login_page")
.loginProcessingUrl("/User/normal/login")
//配置参数名
.usernameParameter("name")
.passwordParameter("password")
//逻辑
.successHandler((httpServletRequest,response,authentication)->{
response.getWriter().write(new JSONObject().fluentPut("msg", authentication.getPrincipal()).toJSONString());
response.getWriter().close();
})
//逻辑
.failureHandler((httpServletRequest,response,authentication)->{
response.getWriter().write("no");
response.getWriter().close();
})
.permitAll()
.and()
.csrf()
.disable();
}
前后端分离注销
.and()
.logout()
//注销url
.logoutUrl("/User//normal/outLogin")
//清除认证信息
.clearAuthentication(true)
//使session失效
//两个都是默认为true
.invalidateHttpSession(true)
//做一些数据处理工作
.addLogoutHandler((httpServletRequest,httpServletResponse,authentication)->{
//通常, LogoutHandler 实现指示能够参与注销处理的类。预计将调用它们以进行必要的清理。因此,他们不应该抛出异常。提供了各种实现
//PersistentTokenBasedRememberMeServices
//TokenBasedRememberMeServices
//CookieClearingLogoutHandler
//CsrfLogoutHandler
//SecurityContextLogoutHandler
})
//注销成功以后的逻逻辑操作
.logoutSuccessHandler((httpServletRequest,httpServletResponse,authentication)->{
httpServletResponse.getWriter().write("login out ok");
})
2配置多个HttpSecurity
对于这个情况,MultiHttpSecurityConfig不需要继承WebSecurityConfigurerAdapter,只要其静态内部类继承这个就好
内部类必须为静态且加上@Configuration和@Order注解,Order表明优先级,越小越优先,没有的优先级最小
主要就是将各个模块分开处理,写一起不利于阅读
@Configuration
//1、Spring Security默认是禁用注解的,要想开启注解, 需要在继承WebSecurityConfigurerAdapter的类上
//加@EnableGlobalMethodSecurity注解, 来判断用户对某个控制层的方法是否具有访问权限
//只有加了@EnableGlobalMethodSecurity(prePostEnabled=true) 那么在上面使用的 //@PreAuthorize(“hasAuthority(‘admin’)”)才会生效
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MultiHttpSecurityConfig{
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("root")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
.roles("ADMIN", "DBA")
.and()
.withUser("admin")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
.roles("ADMIN", "USER")
.and()
.withUser("sang")
.password("$2a$10$eUHbAOMq4bpxTvOVz33LIehLe3fu6NwqC9tdOcxJXEhyZ4simqXTC")
.roles("USER");
}
@Configuration
@Order(1)
public static class AdminSecurityConfig
extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/admin/**").authorizeRequests()
.anyRequest().hasRole("ADMIN");
}
}
@Configuration
public static class OtherSecurityConfig
extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.and()
.csrf()
.disable();
}
}
}
3密码加密
关于密码加密,实际上仅仅直接加密是不足以确保安全的,所以要对其加盐
官方推荐使用BCryptPasswordEncode,它使用BCrypt强哈希函数,开发者提供strength和SecureRandom实例
strength越大,密钥迭代越多,次数为2^strength,strength取值4-31,默认为10
config配置如下
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(10);
}
配置完成后,在内存中配置的密码也必须是加密形式的
明文为123,通过admin和123就可以访问了
.withUser("admin") .password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
.roles("ADMIN", "USER")
加密的简单应用
@Service
public class RegService {
@Autowired
LoginUserMapper loginUserMapper;
public int reg(String username,String password){
return loginUserMapper.insert(new LoginUser.loginBulider()
.name(username)
.password(newBCryptPasswordEncoder() .encode(password))
.build());
}
4方法安全(注解控制权限)
以上的认证与授权都是基于URL的,我们也可以根据注解来配置方法安全
在config类上标注@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
第一个解锁@PreAuthorize和@PostAuthorize,区别就在,一个是先验证,一个是后验证(指方法执行),可以使用基于表达式的语法
第二个解锁@Secured
@Service
public class MethodService {
@Secured("ROLE_ADMIN")
//需要加前缀"ROLE_"
public String admin() {
return "hello admin";
}
@PreAuthorize("hasRole('ADMIN') and hasRole('DBA')")
public String dba() {
return "hello dba";
}
@PreAuthorize("hasAnyRole('ADMIN','DBA','USER')")
public String user() {
return "user";
}
}
5基于数据库的认证
实体类
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
public class LoginUser implements UserDetails {
@TableId(value ="id",type = IdType.AUTO)
private Integer id;
private String name;
private String password;
private Integer role;
private Boolean enable;
private Boolean locked;
@TableField(exist = false)
private String roleString;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(roleString));
return authorities;
}
@Override
public String getUsername() {
return name;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
实现接口的意义在于,默认不需要开发者进行比较信息,如果不匹配会抛出对应异常
service层
实现这个UserDetailService接口的意义在于
通过用户名找数据,找不到抛异常
找到再由系统提供的DaoAuthenticationProvider比对密码
loadUserByUsername在登陆时自动调用
@Service
public class LoginUserService implements UserDetailsService {
@Autowired
private LoginUserMapper loginUserMapper;
@Autowired
private RoleMapper roleMapper;
public LoginUser register(String name, String password)
{
String encode=new BCryptPasswordEncoder(10).encode(password);
LoginUser loginUser = LoginUser.builder()
.name(name)
.password(encode)
.enable(true)
.locked(false)
.role(3)
.build();
loginUserMapper.insert(loginUser);
return loginUser;
}
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
LoginUser loginUser = loginUserMapper.selectOne(new QueryWrapper<LoginUser>().eq("name", name));
if (loginUser == null) {
throw new UsernameNotFoundException("it not exists");
}
loginUser.setRoleString(roleMapper.selectOne(new QueryWrapper<Role>().eq("role_id", loginUser.getRole()))
.getRoleName());
return loginUser;
}
}
对于Spring Security配置类的配置
@Autowired
LoginUserService loginUserService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(loginUserService);
}
6角色继承
在配置类中
在5.0
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
在5.1
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
7几个重要概念
1.SecurityContextHolder
是安全上下文容器
可以在此得知操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
上面的代码是通过SecurityContextHolder来获取到信息,其中getAuthentication()返回了认证信息,再次getPrincipal()返回了身份信息,UserDetails便是Spring对身份信息封装的一个接口。
2.Authentication
源码如下
package org.springframework.security.core;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
- getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
- getCredentials(),证书,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
- getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。
- getPrincipal(),最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。
3.AuthenticationManager
顾名思义,它是认证的一个管理者他是一个接口,里面有个方法authenticate接受Authentication这个参数来完成验证;
4.ProviderManager
实现AuthenticationManager这个接口,完成验证工作。部分源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 维护一个AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
Iterator var6 = this.getProviders().iterator();
//依次来认证
while(var6.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var6.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
         // 如果有Authentication信息,则直接返回
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException var11) {
this.prepareException(var11, authentication);
throw var11;
} catch (InternalAuthenticationServiceException var12) {
this.prepareException(var12, authentication);
throw var12;
} catch (AuthenticationException var13) {
lastException = var13;
}
}
}
}
5DaoAuthenticationProvider:
它是AuthenticationProvider的的一个实现类,非常重要,它主要完成了两个工作,
一个是retrieveUser方法,它返回UserDetails类,看看它的源码
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
UserDetails loadedUser;
try {
     //记住loadUserByUsername这个方法;
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
} catch (UsernameNotFoundException var6) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);
}
throw var6;
} catch (Exception var7) {
throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return oadedUser;
}
}
还有一个项目
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
Object salt = null;
if (this.saltSource != null) {、
      //此方法在你的配置文件中去配置实现的 也是spring security加密的关键 ------划重点
salt = this.saltSource.getSalt(userDetails);
}
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
这个方法的坑点还是挺多的,主要的意思就是拿到通过用户姓名获得的该用户的信息(密码等)和用户输入的密码加密后对比,如果不正确就会报错Bad credentials的错误。为什么说这个方法坑,因为注意到
this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)这里面他自带的一个方法用的是MD5的加密帮你加密在和你存入这个用户时的密码对比,
public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
String pass1 = encPass + "";
String pass2 = this.mergePasswordAndSalt(rawPass, salt, false);
if (this.ignorePasswordCase) {
pass1 = pass1.toLowerCase(Locale.ENGLISH);
pass2 = pass2.toLowerCase(Locale.ENGLISH);
}
return PasswordEncoderUtils.equals(pass1, pass2);
}
可以注意到在生成pass2的时候传入了salt对象,这个salt对象可以通过配置文件去实现,也可以自己写一个实现类来完成。可以说是是和用户输入密码匹配的关键点所在。
6.UserDetails与UserDetailsService
这两个接口在上面都出现了,先看UserDetails是什么:
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
有没有发现它和前面的Authentication接口很像,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。 public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
UserDetailsService和AuthenticationProvider两者的职责常常被人们搞混,关于他们的问题在文档的FAQ和issues中屡见不鲜。记住一点即可,UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。
8session使用
SecurityContextImpl user = (SecurityContextImpl) httpServletRequest.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
LoginUser loginUser = (LoginUser) user.getAuthentication().getPrincipal();
9自定义登录过滤器
如果我想要加入一个验证码机制
生成验证码(伪码)
@RequestMapping("/test/getCode")
public void getCode(HttpServletResponse httpServletResponse,HttpServletRequest httpServletRequest){
VerificationCode ver=new VerificationCode();
httpServletResponse.setContentType(ContentTypeUtil.JPEG.getContentType());
ImageIO.write(ver.getImage, "JPEG", httpServletResponse.getOutputStream());
httpServletRequest.getSession().setAttribute("code", ver.getCode);
}
过滤器
@Component
public class CodeFilter extends GenericFilter {
//实现一个错误处理机制
AuthenticationFailureHandler authenticationFailureHandler=(httpServletRequest,httpServletResponse,e)->{
HashMap<String, Object> hashMap = new HashMap<>();
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(500);
hashMap.put("status",500);
hashMap.put("msg", e.getMessage());
httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString(hashMap));
httpServletResponse.getWriter().flush();
httpServletResponse.getWriter().close();
};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
var httpServletRequest= (HttpServletRequest) servletRequest;
var httpServletResponse = (HttpServletResponse) servletResponse;
if ("post".equalsIgnoreCase(httpServletRequest.getMethod())&&"/login".equals(httpServletRequest.getRequestURI())) {
var answer = httpServletRequest.getParameter("code");
var code =(String)httpServletRequest.getSession().getAttribute("code");
if (answer==null||answer.isBlank()) {
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, new AuthenticationServiceException("empty"));
return;
}
if(!code.equals(answer)){
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, new AuthenticationServiceException("wrong"));
return;
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
然后在config类中注册这个过滤器
@Autowired
CodeFilter codeFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(codeFilter, UsernamePasswordAuthenticationFilter.class);
//do someing
}
10取消spring security自带的登录页面
1设定单独的接口
如果还是.loginPage(“/login”)
@RequestMapping("/login")
public String login()
{
return new JsonObject(true).fluentPut......
.toJsonString'
}
这里就涉及到 Spring Security 中的一个接口 AuthenticationEntryPoint ,该接口有一个实现类:LoginUrlAuthenticationEntryPoint ,该类中有一个方法 commence,如下:
/**
* Performs the redirect (or forward) to the login form URL.
*/
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) {
String redirectUrl = null;
if (useForward) {
if (forceHttps && "http".equals(request.getScheme())) {
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);
if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
}
else {
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
首先我们从这个方法的注释中就可以看出,这个方法是用来决定到底是要重定向还是要 forward,通过 Debug 追踪,我们发现默认情况下 useForward 的值为 false,所以请求走进了重定向。
那么我们解决问题的思路很简单,直接重写这个方法,在方法中返回 JSON 即可,不再做重定向操作,具体配置如下:
2配置类
csrf().disable()
//配置全json回应
.exceptionHandling()
.authenticationEntryPoint((httpServletRequest,httpServletResponse,e)->{
httpServletResponse.setContentType(ContentTypeUtil.JSON.getContentType());
httpServletResponse.getWriter().print(new JSONObject(true).fluentPut("error", e.getMessage())
.fluentPut("class", e.getClass())
.toJSONString());
httpServletResponse.getWriter().flush();
httpServletResponse.getWriter().close();
})
7定时任务
spring自带
启动类加入@EnableScheduling注解开启定时任务注解
普通注解
@Component
public class TestSchedule {
//上一次执行完毕时间点之后多长时间再执行
//单位毫秒,可以fixedDelay也可以fixDelayString,字符串支持占位符
//因为是完毕,所以可以看出是1+5=6s
//delay:2019-10-02T13:36:00.192545100
//delay:2019-10-02T13:36:06.193958800
@Scheduled(fixedDelay = 5000)
public void fixedDelay()
{
System.out.println("delay:"+LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//fixedRate:2019-10-02T13:39:03.810002100
//fixedRate:2019-10-02T13:39:05.810690
//上一次执行完毕时间点之后多长时间再执行,这里就是差2s
//也支持字符串和占位符
@Scheduled(fixedRate = 2000)
public void fixedRate()
{
System.out.println("fixedRate:"+LocalDateTime.now());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//initalDelay指首次执行延迟时间
//no initialDelay:2019-10-02T13:49:49.101805400
//initial:2019-10-02T13:49:50.100502700
@Scheduled(initialDelay = 1000,fixedRate = 2000)
public void initialDelay()
{
System.out.println("initial:"+LocalDateTime.now());
}
}
cron表达式
@Scheduled(cron = "*/5 * * * * ?")
public void cron()
{
System.out.println("cron:"+LocalDateTime.now());
}
一、结构
corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份
二、各字段的含义
| 字段 | 允许值 | 允许的特殊字符 |
|---|---|---|
| 秒(Seconds) | 0~59的整数 | , - * / 四个字符 |
| 分(Minutes) | 0~59的整数 | , - * / 四个字符 |
| 小时(Hours) | 0~23的整数 | , - * / 四个字符 |
| 日期(DayofMonth) | 1~31的整数(但是你需要考虑你月的天数) | ,- * ? / L W C 八个字符 |
| 月份(Month) | 1~12的整数或者 JAN-DEC | , - * / 四个字符 |
| 星期(DayofWeek) | 1~7的整数或者 SUN-SAT (1=SUN) | , - * ? / L C # 八个字符 |
| 年(可选,留空)(Year) | 1970~2099 | , - * / 四个字符 |
注意事项:
每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:
(1)**:表示匹配该域的任意值。假如在Minutes域使用*, 即表示每分钟都会触发事件。
(2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。
(3)-:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
(4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.
(5),:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
(6)L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
(7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。
(8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
(9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。
“/”字符用来指定数值的增量 例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟 在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样
“?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值 当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”
“L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写 但是它在两个子表达式里的含义是不同的。 在天(月)子表达式中,“L”表示一个月的最后一天 在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT
如果在“L”前有具体的内容,它就具有其他的含义了
例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五 注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题
8邮件发送
application.properties配置
spring.mail.host=smtp.qq.com
spring.mail.port=587
spring.mail.default-encoding=UTF-8
spring.mail.username=${E-mail}
spring.mail.password=${code}#授权码
spring.mail.properties.mail.smtp.socketFastory.class=javax.net.ssl.SSLSocketFactor
关于service层
使用异步操纵,可先返回值,然后在后台进行发送操作
@Component
public class MailService {
@Value("${mail.sender}")
String sender;
@Autowired
private JavaMailSender javaMailSender;
@Async
public void sendSimpleMail(String receiver,String subject,String context,String cc){
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setFrom(sender);
if (cc!=null) {
mailMessage.setCc(cc);
}
mailMessage.setSubject(subject);
mailMessage.setText(context);
mailMessage.setTo(receiver);
System.out.println(Thread.currentThread().getName());
javaMailSender.send(mailMessage);
}
@ASync
public void sendAttachFileMail(String receiver, String subject, String context, String cc, File file){
MimeMessage message = javaMailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(sender);
helper.setTo(receiver);
helper.setSubject(subject);
helper.setText(context);
if (cc != null) {
helper.setCc(cc);
}
helper.addAttachment(file.getName(),file);
javaMailSender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
}
}
关于controller层
先注入再使用,直接就是异步操作,初次使用大概200ms,接下来就是20
@Autowired
private MailService mailService;
@RequestMapping("/test")
public void sendMail(){
mailService.sendSimpleMail("B18090512@njupt.edu.cn", "测试", "测试", null);
}
9 mybatis
参考资料https://mybatis.org/mybatis-3/zh/index.html
注解形式
@Mapper
public interface LoginUserMapper{
@Select("select * from login_user where id=#{id}")
public LoginUser getLoginUserById(Integer id);
//主键回写
@Options(userGeneratedKeys=true,keyProperty="id")
@Insert("insert into login_user (username) values(#{username})")
//会自动获取对象中对应字段值
//#{}内要么是形参名,要么是形参对应对象内字段值
public int insertLoginUser(LoginUser loginUser);
@Select("<script> " +
"select username from login_user where id in" +
"<foreach collection='ids' open='(' item='id_' separator=',' close=')'> # {id_}</foreach>\n" +
" </script>")
String[] findUsernameByIds(@Param("ids") int[] ids);
}
使用时只要
@Autowired
private LoginUserMapper loginUserMapper
对于开启驼峰自动转换
@Configuration
public class MybatisConfig{
@Bean
public ConfigurationCustomizer configurationCustomizer(){
return new ConfigurationCustomizer(){
@Overrider
public void customize(Configuration configuration){
configuration.setMapUnderscoreToCamelCase(true)
}
}
}
}
如果不使用@Mapper注解则用@MapperScan扫描所有
//value是文件夹名,一般是com.xxx.xxx.Mapper
@MapperScan(value ="......")
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
xml文件形式
DAO层
@Mapper
public interface LoginUserMapper{
LoginUser find(Integer id);
}
在resource下创建mybatis->mybatis下创建mapper文件夹
结构映射可以使用别名@Alias()注解在类上
或者
<!-- mybatis-config.xml 中 -->
<typeAlias type="com.someapp.model.User" alias="User"/>
<!-- SQL 映射 XML 中 -->
<select id="selectUsers" resultType="User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
ResultMap 最优秀的地方在于,虽然你已经对它相当了解了,但是根本就不需要显式地用到他们。 上面这些简单的示例根本不需要下面这些繁琐的配置。 但出于示范的原因,让我们来看看最后一个示例中,如果使用外部的 resultMap 会怎样,这也是解决列名不匹配的另外一种方式。
在对应mapper.xm文件
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
而在引用它的语句中使用 resultMap 属性就行了(注意我们去掉了 resultType 属性)。比如:
<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.DAO.LoginUserMapper">
<select id="selectLoginUserById" resultType="com.example.demo.Entity.LoginUser">
select * from login_user where id = #{id}
</select>
</mapper>
_______________________________________________________________________
<!-->
或者。其中的键是列名,值便是结果行中的对应值。
如果 User 类型的参数对象传递到了语句中,id、username 和 password 属性将会被查找,然后将它们的值传入预处理语句的参数中。
此处实体类必须有对应getter语句
<!-->
<mapper namespace="com.techmybatis.demo.Mapper.LoginUserMapper">
<select id="find" resultType="hashmap" parameterType="Integer">
select * from login_user where id = #{id}
</select>
</mapper>
_______________________________________________________________________
<!-->
主键回写
<!-->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO login_user (username,password,student_number)VALUE (#{username},#{password},#{studentNumber})
</insert>
批量sql(注解里面也能这么写)
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO login_user (username,password,student_number)VALUE
<foreach collection="loginUsers" item="item" separator="," >
(#{item.username},#{item.password},#{item.studentNumber})
</foreach>
</insert>
对应Mapper
Integer insert(@Param("loginUsers") LoginUser[] loginUser
@Param注解
如果你的映射方法的形参有多个,这个注解使用在映射方法的参数上就能为它们取自定义名字。若不给出自定义名字,多参数(不包括 RowBounds 参数)则先以 "param" 作前缀,再加上它们的参数位置作为参数别名。例如 #{param1}, #{param2},这个是默认值。如果注解是 @Param("person"),那么参数就会被命名为 #{person}。
配置
spring.datasource.driver-class-userName=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.url.=jdbc:mysql://localhost:3306/second_hand?characterEncoding=UTF-8&serverTimezone=GMT%2B8
mybatis.mapper-locations=/mybatis/mapper/*.xml
动态sql
xml文件
if语句
<select id="find" resultType="com.techmybatis.demo.Entity.LoginUser" >
select * from login_user where enable = 1
<if test="username !=null">
AND username= #{username}
</if>
</select>
————————————————————————————————————————————————————————————————————————
<select id="find" resultType="com.techmybatis.demo.Entity.LoginUser" >
select * from login_user where enable = 1
<if test="username !=null">
AND username= #{username}
</if>
<if test="loginUser!=null and loginUser.studentNumber!=null">
AND student_number= #{loginUser.studentNumber}
</if>
</select>
DAO层
LoginUser find(String username);
——————————————————————————————————————————————————
LoginUser find(String username,LoginUser loginUser);
对应语句,也就是说如果传入的username不为空则在后面加AND语句(支持like模糊搜索)
loginUserMapper.find(null);
select * from login_user where enable = 1
loginUserMapper.find(“sun”);
select * from login_user where enable = 1 AND username= ?
//两个if是并列的,满足多少加多少
LoginUser user = LoginUser.builder().studentNumber("0").build();
LoginUser loginUser = loginUserMapper.find(null,user);
System.out.println(loginUser);
select * from login_user where enable = 1 AND student_number= ?
choose语句
<select id="find" resultType="com.techmybatis.demo.Entity.LoginUser" >
select * from login_user where enable = 1
<choose>
<when test="username!=null">
AND username=#{username}
</when>
<when test="loginUser!=null and loginUser.password!=null" >
AND password=#{loginUser.password}
</when>
<otherwise>
AND student_number =#{studentNumber}
</otherwise>
</choose>
DAO层
LoginUser find(String username,LoginUser loginUser,String studentNumber);
实验
//类似于switch结构
LoginUser user = LoginUser.builder().password("0").studentNumber("0").build();
LoginUser loginUser = loginUserMapper.find(null,user,user.getStudentNumber());
select * from login_user where enable = 1 AND password=?
trim,where,set语句
如果默认语句where字句缺失,比如
<select id="find" resultType="com.techmybatis.demo.Entity.LoginUser" >
select * from login_user where
<if test="username !=null">
username= #{username}
</if>
<if test="loginUser!=null and loginUser.studentNumber!=null">
AND student_number= #{loginUser.studentNumber}
</if>
</select>
如果没有匹配,就会导致sql变为 SELECT * FROM login_user where
如果只有第二个匹配,
SELECT * FROM BLOG
WHERE
AND student_number ="xxxxx"
方便起见可以使用
where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入“WHERE”子句。而且,若语句的开头为“AND”或“OR”,where 元素也会将它们去除。
<select id="find" resultType="com.techmybatis.demo.Entity.LoginUser">
select * from login_user
<where>
<if test="username!=null">
username=#{username}
</if>
<if test="enable!=null">
AND enable=#{enable}
</if>
</where>
</select>
DAO层
ArrayList<LoginUser> find(String username, Boolean enable);
测试
loginUserMapper.find(null, true);
select * from login_user WHERE enable=?
如果 where 元素没有按正常套路出牌,我们可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
prefixOverrides 属性会忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它的作用是移除所有指定在 prefixOverrides 属性中的内容,并且插入 prefix 属性中指定的内容。
类似的用于动态更新语句的解决方案叫做 set。set 元素可以用于动态包含需要更新的列,而舍去其它的。比如:
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
这里,set 元素会动态前置 SET 关键字,同时也会删掉无关的逗号,因为用了条件语句之后很可
能就会在生成的 SQL 语句的后面留下这些逗号。(译者注:因为用的是“if”元素,若最后一个“if”没有匹配上而前面的匹配上,SQL 语句的最后就会有一个逗号遗留)
若你对 set 元素等价的自定义 trim 元素的代码感兴趣,那这就是它的真面目:
<trim prefix="SET" suffixOverrides=",">
...
</trim>
注意这里我们删去的是后缀值,同时添加了前缀值。
foreach
动态 SQL 的另外一个常用的操作需求是对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
<!-->
以(开头,每一项分割为,以)结尾 (()是,遍历完的)
<!-->
#{item}
</foreach>
</select>
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及在迭代结果之间放置分隔符。这个元素是很智能的,因此它不会偶然地附加多余的分隔符。
注意 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象传递给 foreach 作为集合参数。当使用可迭代对象或者数组时,index 是当前迭代的次数,item 的值是本次迭代获取的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
如何使用注解实现动态sql
要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素
就和写xml一样
@Select("<script> " +
"select username from login_user where id in" +
"<foreach collection='ids' open='(' item='id_' separator=',' close=')'> # {id_}</foreach>\n" +
" </script>")
String[] findUsernameByIds(@Param("ids") int[] ids);
或者使用sqlProvider
@SelectProvider(type = SqlProvdier.class ,method = "sqlSelect")
LoginUser selectById(LoginUser loginUser);
这个类叫啥都行
public class SqlProvdier {
public String sql(LoginUser loginUser){
return "select * from login_user where id="+loginUser.getId();
}
public String sqlSelect(final LoginUser loginUser){
var sql = new SQL();
sql.SELECT("*").FROM("Login_user")
.WHERE("id=#{id}");
if (loginUser.getRole()!=null) {
sql.WHERE("role =#{role}");
}
return sql.toString();
}
}
SELECT:表示要查询的字段,如果一行写不完,可以在第二行再写一个SELECT,这两个SELECT会智能的进行合并而不会重复
FROM和WHERE:跟SELECT一样,可以写多个参数,也可以在多行重复使用,最终会智能合并而不会报错
这样语句适用于写很长的SQL时,能够保证SQL结构清楚。便于维护,可读性高。但是这种自动生成的SQL和HIBERNATE一样,在实现一些复杂语句的SQL时会束手无策。所以需要根据现实场景,来考虑使用哪一种动态SQL
@Param注解
@Param是MyBatis所提供的(org.apache.ibatis.annotations.Param),作为Dao层的注解,作用是用于传递参数,从而可以与SQL中的的字段名相对应,一般在2=<参数数<=5时使用最佳。
当DAO层方法函数中存在多个形参,尤其是多个javabean时,必须使用@Param注解区分
同时不必使用parameterType属性指定参数类型
它和@RequestParam没关系
一对一
最简单的方法就是创建一个具有所有对应字段的实体类(除有必要,勿增实体)
比如说一个留言板对应一个用户,我想顺便获取他的数据,一次IO
配置映射
只要配置数据库和实体类字段名不一致的的(驼峰如果打开了就不用了)
如果没有autoMapping="true"就要手动配置每一项
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.techmybatis.demo.Mapper.AppointmentMapper">
<resultMap id="AppointmentResultMap" type="com.techmybatis.demo.Entity.AppointmentWithLoginUser" autoMapping="true">
<id column="id" property="Id"/>
<result column="user_name" property="userName"/>
<result column="appointment_time" property="appointmentTime"/>
<result column="time_stamp" property="timeStamp"/>
<result column="real_name" property="realName"/>
<result column="student_ID" property="studentID"/>
<association property="loginUser" javaType="com.techmybatis.demo.Entity.LoginUser" autoMapping="true">
<id column="id" property="id"></id>
<result column="real_name" property="realName"/>
</association>
</resultMap>
<select id="selectById" resultMap="AppointmentResultMap">
SELECT a.* , l.*
FROM appointment AS a JOIN login_user AS l
ON l.username=a.user_name
WHERE a.id=#{id}
</select>
<select id="select" resultType="com.techmybatis.demo.Entity.Appointment">
select * from appointment where id=#{id}
</select>
</mapper>
修改pojo
加入对象的引用
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
@ToString
public class AppointmentWithLoginUser {
private Integer Id;
@NotBlank(message = "counselor is empty")
private String counselor;
private String userName;
@NotBlank(message = "phone is empty")
@Size(min = 11,max = 16,message = "phone format is wrong")
private String phone;
@NotBlank(message = "appointmentTime is empty")
private String appointmentTime;
private String remark;
@NotBlank(message = "realName is empty")
private String realName;
private String state;
private String timeStamp;
@NotBlank(message = "sex is empty")
@Pattern(regexp = "M|F",message = "wrong sex,please choose M or F")
private String sex;
@NotBlank(message = "academy is blank")
private String academy;
@NotBlank(message = "student ID is blank")
private String studentID;
private LoginUser loginUser;
}
其实我觉得没必要增加一个含所有字段的实体,但是有必要增加一个含对应引用的实体类
一对多
<resultMap id="LoginUserWithAppointmentResultMap" type="com.techmybatis.demo.Entity.LoginUserWithAppointment" autoMapping="true">
<id column="lid" property="id"></id>
<collection property="appointments" ofType="com.techmybatis.demo.Entity.Appointment" autoMapping="true">
<id column="aid" property="id"></id>
</collection>
</resultMap>
<select id="selectLoginUser" resultMap="LoginUserWithAppointmentResultMap">
select a.*,l.*,a.id 'aid',l.id 'lid' from login_user as l left join appointment as a on l.username=a.user_name
where l.username=#{username}
</select>
对应实体类
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
@EqualsAndHashCode
public class LoginUserWithAppointment {
private Integer id;
private String username;
private String password;
private String role;
private String nickname;
private String realName;
private List<Appointment> appointments;
}
10 AOP
依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
被增强的类
@Service
public class AOPTestService {
@Value("${AOPTest.enable}")
Boolean enable;
@Qualifier("CDImplSecond")
@Autowired
CD cd;
public CD dod (String id,Character c,Long b)throws Exception{
if (enable) {
throw new Exception("啊,我异常了");
}
System.out.println("dod执行了"+Integer.toHexString(hashCode()));
System.out.println("id:"+id);
return cd;
}
public void getUserById(Integer id){
//todo do something to select user info from db
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("getUserById执行了");
}
}
切面定义
public class MyPointcut {
//表达式依次为访问控制符(可空) 返回值(包装类和基础不是一种) 类全名 方法名 参数列表
//*依次为任意返回值 某包下任意类 任意方法 (..)指参数任意 支持正则
//@Pointcut("execution(* com.techmybatis.demo.AOP.*.*(..))")
//空的,有内容也不执行
@Pointcut("execution(public * com.techmybatis.demo.AOP.AOPTestService.dod(String,Character,Long))")
public void pc1()
{
}
}
切面表达式
1:@Pointcut("execution(public * com.techmybatis.demo.AOP.AOPTestService.dod(String,Character,Long))")
2:@annotation()
增强方法
@Aspect
@Component
//这里统称为通知--advice
public class LogAspect {
//getThis和getTarget区别
////返回AOP代理对象,也就是com.sun.proxy.$Proxy18
// Object getTarget();
// 返回目标对象,一般我们都需要它或者(也就是定义方法的接口或类,为什么会是接口呢?这主要是在目标对象本身是动态代理的情况下,例如Mapper。所以返回的是定义方法的对象如aoptest.daoimpl.GoodDaoImpl或com.b.base.BaseMapper<T, E, PK>)
//这个信息是被切入的信息,执行前调用
//getSignature获取方法签名
//value为切面的全名,如果在同一类下可以不写
//对应实现类为org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint
@Before(value ="com.techmybatis.demo.AOP.MyPointcut.pc1()")
public void before(JoinPoint joinPoint){
System.out.println("before!!!!!!!!!!!!");
System.out.println(joinPoint.getThis());
System.out.println(joinPoint.getTarget());
System.out.println("kind:"+joinPoint.getKind());
System.out.println("getArgs");
//获得的是实际参数
for (Object arg : joinPoint.getArgs()) {
System.out.print(arg+" ");
}
System.out.println("\nname:"+joinPoint.getSignature().getName());
System.out.println("modifier:"+Modifier.toString(joinPoint.getSignature().getModifiers()));
System.out.println("getDeclaringType"+joinPoint.getSignature().getDeclaringType());
System.out.println("getDeclaringTypeName"+joinPoint.getSignature().getDeclaringTypeName());
System.out.println("target"+joinPoint.getTarget());
}
//执行后
@After(value = "com.techmybatis.demo.AOP.MyPointcut.pc1()")
public void after(JoinPoint joinPoint){
System.out.println("after!!!!!!!!!!!!");
System.out.println("现在的实际参数");
for (Object arg : joinPoint.getArgs()) {
System.out.print(arg+" ");
}
}
//返回之后调用 returning和下面参数列表的形参中一致 Object指返回值任意,毕竟所有的都可以
//向上转型为Object 否则就必须对应
//简单来说就类似于 ? extends #{returnType}
//若中断则不执行(比如是异常抛出)
@AfterReturning(value = "com.techmybatis.demo.AOP.MyPointcut.pc1()",returning = "r")
public void afterReturn(JoinPoint joinPoint, CDImplSecond r){
System.out.println("after returning!!!!!!!!!!!!!!" );
System.out.println("返回值为:"+r);
r.builder=new StringBuilder("修改了");
}
//无异常就不捕获
//上面可以看出,虽然AfterThrowing可以对目标方法的异常进行处理,但这种处理方法和直接使用catch捕捉不同,如下:
//catch:意味着完全处理该异常,如果catch语句中没有重新抛出新异常,则该方法可以正常结束;
//AfterThrowing:虽然处理了该异常,但它不能完全处理异常,该异常依然会传播到上一级调用者
@AfterThrowing(value = "com.techmybatis.demo.AOP.MyPointcut.pc1()",throwing = "e")
public void afterThrow(JoinPoint joinPoint, Exception e){
System.out.println("尝试捕获异常");
System.out.println(e.getMessage());
}
//不执行或不返回就会导致空指针异常而且@Before不执行
//before 前around
@SneakyThrows
@Around(value = "com.techmybatis.demo.AOP.MyPointcut.pc1()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
System.out.println("around!!!!!!!!!!!!!!");
//返回值
//修改实参在before前,也就是会修改入参
Object o = proceedingJoinPoint.proceed(new Object[]{"String",'c',100L});
return o;
}
}
11异步
简单异步
所在类必须加入IOC容器
@Component
public class AsyncTest {
@Async
public String selectById(String s){
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
}
System.out.println(LocalDateTime.now()+" "+s);
return s;
}
}
入口类加入@EnableAsync注解
@SpringBootApplication
@EnableCaching
@EnableAsync
public class Mybatis授课Application {
public static void main(String[] args) {
SpringApplication.run(Mybatis授课Application.class, args);
}
}
回调异步
和future一样
@Async
public Future<String> updateById2(String s){
System.out.println(Thread.currentThread());
System.out.println("2"+LocalDateTime.now().toString());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
}
System.out.println(LocalDateTime.now().toString());
return new AsyncResult<>(LocalDateTime.now().toString());
}
线程池
@Configuration
@EnableAsync
public class MyAsyncConfiguration implements AsyncConfigurer {
@Autowired
private AsyncUncaughtExceptionHandler asyncUncaughtExceptionHandler;
@Override
public Executor getAsyncExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("async-task-name-%d")
.build();
int availableProcessors = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(availableProcessors + 1, 2 * availableProcessors, 30L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(125), threadFactory);
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
//拦截非future方法
return asyncUncaughtExceptionHandler;
}
}
@Component
class GlobalAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
//todo
}
}
12缓存
配置文件
在resource下默认位置写配置文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<defaultCache
maxElementsInMemory="100000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
overflowToOffHeap="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
></defaultCache>
<cache name="login_cache"
maxElementsInMemory="100000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
overflowToOffHeap="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
></cache>
对应方法
@Service
@CacheConfig(cacheNames = "login_cache")
public class CacheTest {
//更新
@CachePut(key = "#loginUser.id")
public LoginUser updateById(LoginUser loginUser){
return new LoginUser();
}
@Cacheable(key = "#username")
public LoginUser Login(String username){
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("没有缓存");
return LoginUser.builder().username(username)
.password(UUID.randomUUID().toString())
.build();
}
//删除对应缓存
@CacheEvict(key = "#username")
public void delete(String username){
}
}
Spring自带key生成
当然,Spring Cache 中提供了root对象,可以在不定义 keyGenerator 的情况下实现一些复杂的效果,root 对象有如下属性:
自生成key
@Component
public class MyKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName()+Arrays.toString(params);
}
}
//然后在方法上使用该 keyGenerator :
@Cacheable(keyGenerator = "myKeyGenerator")
public User getUserById(Long id) {
User user = new User();
user.setId(id);
user.setUsername("lisi");
System.out.println(user);
return user;
}
13 Json
fastjson
总论
就是非常快的json库
主要类
JSONObject
我们可以看出实际上就是实现了map接口,所以大部分操作都是对map操作,即大部分方法和map方法一致
对于所有的get操作第二个参数是一个Class类带泛型,所以返回值类型也就确定了
对于生成的键值对顺序,默认为无序的,通过构造参数(boolean ordered)可以该为有序。其底层实现为有序则map参数为LinkedHashMap,
无序则为HashMap(因此没有线程安全性),其传入的大小参数直接反应为map的大小,默认为16。
private static final long serialVersionUID = 1L;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private final Map<String, Object> map;
public JSONObject() {
this(16, false);
}
public JSONObject(Map<String, Object> map) {
if (map == null) {
throw new IllegalArgumentException("map is null.");
} else {
this.map = map;
}
}
public JSONObject(boolean ordered) {
this(16, ordered);
}
public JSONObject(int initialCapacity) {
this(initialCapacity, false);
}
public JSONObject(int initialCapacity, boolean ordered) {
if (ordered) {
this.map = new LinkedHashMap(initialCapacity);
} else {
this.map = new HashMap(initialCapacity);
}
}
主要api
//序列化
String jsonString =JSON.toJSONString(obj);
//反序列化
Type type=JSON.parseObject(String,Type.class);
javabean与json互相转化
Person person = new Person("1","fastjson",1);
//这里将javabean转化成json字符串
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);
//这里将json字符串转化成javabean对象,
person =JSON.parseObject(jsonString,Person.class);
System.out.println(person.toString());
//若对应属性为null则转化的json中对应属性也是null
list与json互相转换
Person person1 = new Person("1","fastjson1",1);
Person person2 = new Person("2","fastjson2",2);
List<Person> persons = new ArrayList<Person>();
persons.add(person1);
persons.add(person2);
String jsonString = JSON.toJSONString(persons);
System.out.println("json字符串:"+jsonString);
//解析json字符串
List<Person> persons2 = JSON.parseArray(jsonString,Person.class);
//输出解析后的person对象,也可以通过调试模式查看persons2的结构
System.out.println("person1对象:"+persons2.get(0).toString());
System.out.println("person2对象:"+persons2.get(1).toString());
JSON格式字符串与简单对象型转换
//JSON字符串是否被压缩与结果无关
String json="{\n" +
" \"name\": \"BeJson\",\n" +
" \"url\": \"http://www.bejson.com\",\n" +
" \"page\": 88\n" +
"}";
JSONObject jsonObject=JSONObject.parseObject(json);
System.out.println("name:"+jsonObject.getString("name")+"\n"+"url:"+jsonObject.getString("url")
+"\n"+"page:"+jsonObject.getString("page"));
System.out.println(jsonObject.getString("page").getClass());
System.out.println(jsonObject.getInteger("page").getClass());
//控制台结果
//name:BeJson
//url:http://www.bejson.com
//page:88
//class java.lang.String
//class java.lang.Integer
//由此可见get**()方法返回值和**有关
复杂JSON格式字符串的转换
String json="{\"name\":\"BeJson\",\"url\":\"http://www.bejson.com\",\"page\":88,\"isNonProfit\":true,\"address\":{\"street\":\"科技园路.\",\"city\":\"江苏苏州\",\"country\":\"中国\"},\"links\":[{\"name\":\"Google\",\"url\":\"http://www.google.com\"},{\"name\":\"Baidu\",\"url\":\"http://www.baidu.com\"},{\"name\":\"SoSo\",\"url\":\"http://www.SoSo.com\"}]}";
JSONObject links = (JSONObject) JSONObject.parseObject(json)
.getJSONArray("links")
.get(1);
System.out.println(links.getString("name"));
//因为getJSONArray返回值为JSONObject对象然后调用get()方法得到的是Object对象所以要强制类型转换
在json里面添加一个键值
put()方法
public Object put(String key, Object value) {
return this.map.put(key, value);
}
JSONObject jsonObject=new JSONObject(true);
jsonObject.put("message", "success");
jsonObject.put("loginUser", new LoginUser("ad", "sda", "ads")); System.out.println(jsonObject.toJSONString());
//如果传入的对象的get方法返回值和对应属性类型不一致则会抛
//出com.alibaba.fastjson.JSONException: write javaBean error, fastjson version 1.2.58, class com.example.demo.entity.LoginUser, fieldName : loginUser错误
//这个错误是我属性为Integer返回值为int发现的
//不过现在getset方法都是生成的,还是在修改某一属性的时候重新生成吧,或者直接@Data(lombok)算了
fluentPut()方法
可以连续添加
public JSONObject fluentPut(String key, Object value) {
this.map.put(key, value);
return this;
}
putAll()方法
public void putAll(Map<? extends String, ? extends Object> m) {
this.map.putAll(m);
}
fluentPutAll()方法
public JSONObject fluentPutAll(Map<? extends String, ? extends Object> m) {
this.map.putAll(m);
return this;
}
jackson
普通序列化/反序列化
String content="{\n" +
"\"user_name\":\"username\",\n" +
"\"password\":\"password\",\n" +
"\"user_id\":\"13\",\n" +
"\"role\":\"ROLE_ADMIN\"\n" +
"}\n" +
"\n";
//也可以直接传入map
LoginUser s = new ObjectMapper().readValue(content, LoginUser.class);
System.out.println(s);
s.setUserId(1L);
s.setRole(ROLE.ADMIN.getRole());
System.out.println(new ObjectMapper().writeValueAsString(s));
初步定制
@JsonIgnoreProperties中ignoreUnknown指是否实例化不存在的字段
allowSetters是指是否反序列化
allowGetters是指是否能够序列化
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true,value = {"questionRecordId","timeStamp"},allowSetters = false,allowGetters = true)
public class QuestionRecord implements Serializable {
@JsonAlias("question_record_id")
private Long questionRecordId;
@JsonAlias("time_stamp")
private java.sql.Timestamp timeStamp;
@JsonAlias("is_recognized")
private Boolean isRecognized;
@JsonAlias("is_solved")
private Boolean isSolved;
@JsonAlias("is_timely")
private Boolean isTimely;
@JsonAlias("is_artificial_solved")
private Boolean isArtificialSolved;
@JsonAlias("satisfaction_degree")
private Long satisfactionDegree;
private String assessment;
private String content;
@JsonAlias("customer_name")
private String customerName;
}
定制化
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonSerialize(using = ChatRecordSerializer.class)
@JsonDeserialize(using = ChatRecordDeSerializer.class)
//@JsonIgnoreProperties(ignoreUnknown = true,allowGetters = true,value = "chatId")
public class ChatRecord implements Serializable {
@TableId(type = IdType.AUTO)
private Long chatId;
@NotBlank(message = "内容不能为空")
private String content;
private java.sql.Timestamp timeStamp;
@JsonAlias(value = "customer_name")
@NotBlank(message = "用户名不能为空")
private String customerName;
}
class ChatRecordSerializer extends JsonSerializer<ChatRecord>{
@Override
public void serialize(ChatRecord record, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeNumberField("chatId", record.getChatId());
jsonGenerator.writeStringField("content", record.getContent());
jsonGenerator.writeObjectField("time_stamp", record.getTimeStamp());
jsonGenerator.writeStringField("customer_name", record.getCustomerName());
jsonGenerator.writeEndObject();
}
}
class ChatRecordDeSerializer extends JsonDeserializer<ChatRecord>{
@Override
public ChatRecord deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser);
String content = ((TextNode) treeNode.get("content")).asText();
String customerName= ((TextNode) treeNode.get("customer_name")).asText();
return ChatRecord.builder()
.customerName(customerName)
.content(content)
.build();
}
}
坑
关于mapper
应该存在@mapper和@Repository注解
关于service
其实现类应该用@autowired注入使用,而不是new
new出来后的实例与springboot管理后注入进来的实例不是同一个(一个被初始化了,一个未被初始化),导致,报了空指针的错误。以后记得只要是用自动注入就别再自己new同样的实例了。
关于org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
1检查namespace
2检查sql语句id
3检查配置文件读取xml
4检查xml是否有xml结尾
关于spring security 修改session数据
遇到场景 : 前端通过session获取当前用户信息,当前用户信息在前端页面发生了改变时(比如用户update了自己的email属性);可能数据库里面已经update了,而程序没有重启或者用户没有重新登陆,则session中的值是不会发生改变的,即验证的用户信息也是不变的,前端显示也并不会更新。
一般spring security在认证后,security会把一个SecurityContextImpl对象存储到session中,此对象中有当前用户的各种资料。
基本思路 : 我们要改变SpringSecurity 的session中用户信息,本质上就是要改SecurityContextImpl对象中的用户信息。
- SecurityContextImpl中只有两个基本业务方法,getAuthentication() / setAuthentication() ,这两个方法是为了获取/设置 Authentication 对象 ,Authentication 是一个接口。
2 . 用户认证最核心的部分是接口 Authentication接口,它有两个最重要的方法 getPrincipal() / setPrincipal(),可获取被验证的用户的身份 / 可用于设置被验证的用户的身份。
3 . Authentication有一个基本的实现类,UsernamePasswordAuthenticationToken,我们只要将它初始化出来,并将我们更新的用户信息赋上去,即可完成修改session的用户信息。
操作原理
在Spring Security中是这样处理这三部分的:
1.username和password被获得后封装到一个UsernamePasswordAuthenticationToken(Authentication接口的实例)的实例中
2.这个token被传递给AuthenticationManager进行验证
3.成功认证后AuthenticationManager将返回一个得到完整填充的Authentication实例
4.通过调用SecurityContextHolder.getContext().setAuthentication(...),参数传递authentication对象,来建立安全上下文(security context)
所以说要构造一个新UserbnamePasswordAuthenticationToknen
但是这种会导致授权失效
//1.从HttpServletRequest中获取SecurityContextImpl对象
SecurityContextImpl securityContextImpl =(SecurityContextImpl)request.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
//2.从SecurityContextImpl中获取Authentication对象
Authentication authentication = securityContextImpl.getAuthentication();
//3.初始化UsernamePasswordAuthenticationToken实例 ,这里的参数user就是我们要更新的用户信息
//更新后与user完全一致,所以要全部赋值
UsernamePasswordAuthenticationToken accountInfo = new UsernamePasswordAuthenticationToken(user, authentication.getCredentials());
accountInfo.setDetails(authentication.getDetails());
//4.重新设置SecurityContextImpl对象的Authentication
securityContextImpl.setAuthentication(auth);
或者直接修改里面的实体类
@RequestMapping("/counselor/hello")
public String test(HttpServletRequest httpServletRequest){
SecurityContextImpl user = (SecurityContextImpl) httpServletRequest.getSession().getAttribute("SPRING_SECURITY_CONTEXT"); Authentication authentication = user.getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
loginUser.setTemp("ads");
return "hello"+ ((LoginUser)user.getAuthentication().getPrincipal()).toString();
}
关于无法json化的问题(fastjson)
.fluentPut("allInformation",allMessageBroad.stream().map(MessageBroad::show).collect(Collectors.toList()))
其中show方法
public MessageBroadShowed show(){
return new MessageBroadShowed(this);
}
@ToString
@Data
public class MessageBroadShowed{
private Long id;
private String messages;
private String time;
private MessageBroadShowed(MessageBroad messageBroad){
this.id=messageBroad.id;
this.messages=messageBroad.messages;
this.time= messageBroad.time;
}
如果没有@Date注解就会导致此对的值无内容
[{},{},{},{},{}]
实际上是需要存在get方法才能确保转换为json字符串
关于全局session hashmap值在postman里面重复的问题
public static ConcurrentHashMap<String,HttpSession> ALLSession=new ConcurrentHashMap();
//登录模块的一条语句
SessionHashMap.ALLSession.put(userFromDB.getUsername(),httpServletRequest.getSession());
如果使用postman先后登录两个账户则
K:li V:LoginUser(Id=12, username=li, password=123456, role=counselor)
K:sun V:LoginUser(Id=12, username=li, password=123456, role=counselor)
大概是因为cookie相一致的原因。。。。
只要一个终端只登录一个账户,不会出现这个问题
关于mybatisDAO接口返回值为List导致java.lang.UnsupportedOperationException的问题
resultType代表的是List中的元素类型,而不应该是List本身,不要对于dao接口生命的List 就误以为返回的是list,返回的应该是元素本身的类型
应该是
<select id="find" resultType="com.techmybatis.demo.Entity.LoginUser">
而非
<select id="find" resultType="java.util.arrayList">
关于org.springframework.http.converter.HttpMessageNotReadableException
一个请求,只有一个RequestBody
参数验证message失效
如果这个BindResult放在参数最后,前面存在多个要校验的就会在controller层报错
(@Validated RequireSecond requireSecond, BindingResult bindingResult,
MultipartFile multipartFile)
@PostMapping("/public/test")
public String fun(@Validated RequireSecond requireSecond,
MultipartFile multipartFile,
BindingResult bindingResult
){
if (bindingResult.hasErrors()) {
return JSONObject.toJSONString(bindingResult.getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList()));
}
return "ok";
}
一个服务器上部署两个Spring boot的web项目,在同一个浏览器上同时登录两个系统时,session会错乱,导致系统无法使用。
应该是两个系统使用了同一个session id导致的问题
解决办法:配置文件中增加server.servlet.session.cookie.name=xxx 将两个系统的cookie name设置成不同名称即可