分类 编程语言 下的文章

  1. 实际上SpringBoot是可以结合JSP使用的,不过需要加上providedRumtime("org.apache.tomcat.embed:tomcat-embed-jasper")作为JSP解析器。然而使用了JSP特性页面可能会出现编译错误,这个时候SpringBoot显示出来的是404。
  2. Thymeleaf的直接取值表达式是[[${val}]]
  3. 验证登录的时候,最好使用@{/login}获取Spring Security的登录地址,不然登录似乎会失败。
  4. Thymeleaf需要从application.properties一类配置文件中读取数据的时候,传统方法是先使用@Value{key.name}注入到对应类型的变量中,然后再用MVC的ModeladdAttribute()方法设置后传给对应的模板。
  5. 退出登录也要POST方法。
  6. 登陆/退出(默认URLlogin/logout)成功后自动跳转的方法名分别为defaultSuccessUrllogoutSuccessUrl。其中defaultSuccessUrl有第二个选填参数表示是否永远跳转至该页面。
  7. 别想着明文密码存储了,Spring Security 5删除了PlaintextPasswordEncoder,现在基本上都是用BCrypt,到网上随便找一个BCrypt加密工具来生成测试账户的密码吧。(或者用AuthenticationManagerBuilder.withUser直接在代码里也行,这里可以用明文)
  8. 可以通过重写UserDetailsUserDetailsService来重定向登陆所用的用户信息类到自己想要的方式。比如写一个连接数据库的Entity,加上jdbcAuthentication()(说实话,这个真的有必要吗),就可以实现从数据库中获取登录信息。进行鉴权处理的是Authentication类,所需要的UserDetails信息是约定俗成usernamepassword两个属性,其中password是存在数据库中的、使用约定(可以设置)的加密方式加密后的串。(不记得从哪看来的了,说是spring的原则是约定高于配置) 参考:https://www.baeldung.com/spring-security-authentication-with-a-database 以及附带的github代码
  9. 如果在post表单里用了th:object的话,在对应的Controller方法中要用@ModelAttribute(modelName)获得。
  10. Kotlin的foreach用的是大括号,可以使用it获取当前次循环正使用的元素。
  11. 若是对RequestMapping方法中,有javax.validation验证限制的对象使用@Valid注解的话,可以按照预期检查出错误来,但是由于validation本身发生在进入方法体之前,方法本身并不会被执行。需要使用ExceptionHandler来捕获异常,若是对数据传输对象Data Transfer Object, DTO使用@Valid的话,会产生org.springframework.validation.BindException(噗,stackoverflow上有个老哥写成了java.net.BindException
  12. Kotlin会给方法自动生成getter和setter,但是仅仅是作用和Java中的一样,并不能通过getXXX()方法来访问属性= =
  13. Kotlin通过反射调用方法有点麻烦,可以绕道Java,通过::class.java.getMethod().invoke()实现等效的功能。
  14. Thymeleaf在使用@{}生成链接地址的时候,可以通过特殊的方式生成含变量的路径,格式形如@{/path/{key}(key=${val})}会生成/path/${val}形式的地址。
  15. URL作为变量要用@PathVariable而不是@PathParam。以及为了防止万一,最好传入封装类型而不是基本类型。
  16. 可以使用th:checked属性设置输入复选框的初值。

在看Spring WebFlux的时候,看到了这么一段代码:

public interface Publisher<T> 
{
    public void subscribe(Subscriber<? super T> s);
}

之前在写Java程序的时候,看到部分Java内置类型中就出现过类似的表达,即<? super T>。当时也没考虑过这玩意具体有什么用,如果仅仅只是看表达式的话,很容易理解它要表达的基础意思:这里要传入一个类,要求该类是T的父类。但是有什么用呢?怕你传入一个T的子类?虽然这么说听起来的确有点道理,但显然这个设计要表达的不是这个意思。

基础介绍可以参见这两篇还行的文章: https://blog.csdn.net/qq_29951485/article/details/88068338 https://www.cnblogs.com/hoojjack/p/6817547.html?utm_source=itdadao&utm_medium=referral

总而言之目的是提供泛型类之间的继承关系。很容易从文章中看出的是,编译器不会认泛型参数之间的继承关系,因而Sun的程序员们决定给泛型提供superextends两个通配符关键字来人为声明泛型参数类的继承关系。注意使用时一同添加的?的含义,虽然表示任意,但是不是任意类型,而是任意确定类型。好比摸奖的时候,手到箱子里抓了一个球,你没有办法口胡说摸了特等奖,摸了是啥就是啥,因为只要手从箱子里出来之后,是什么球大家都知道得清清楚楚。

但是显然非一一对应的映射是没有反函数的,因而就会出现这么一个问题:泛型类操作时的参数类型究竟是什么?显然如果这个问题不明确的话只有两种可能:编译器找不到合适的方法操作数据,罢工;编译器随性选了一种不合适的方法操作数据,搞砸。

所以这个时候就要通过使用的通配符,以及面向对象设计的基本原理来解决这个问题了。接下来用?代指使用泛型通配符时传入的具体类别,则对于<? extends Parent>,根据面向对象的特性有:

  1. ?类为任意固定的Parent类或其子类;
  2. ?类一定可以转换为Parent类(及其父类)的合法对象。

则根据第一条,可以发现,由于在Java中没有提供明确?类型的方法,出于安全考虑,实际上是不允许向<? extends Parent>类型的形参传入任何值的。相反,具有<? extends Parent>类型返回值的方法返回的都是Parent类型的对象。

因而,相当于使用了extends通配符的泛型类对象的所有泛型输入方法全部失效,泛型输出方法正常工作,但返回的是被extends的类对象。

那我要传入对象怎么办?

答案是反过来,使用super。而super又与extends恰好相反。在<? super Child>泛型类中:

  1. 任意固定的Child类或其父类为?类;
  2. Child类(及其子类)一定可以转化为?类的合法对象。

这也造成了与上面恰好相反的效果:使用了super通配符的泛型类对象的所有泛型输出方法全部失效,泛型输入方法正常工作,允许传入被super的类或其子类对象。

目前个人感觉super是为了弥补extends的缺陷,而不是作为设计的新功能而出现的。

红 蓝 黑 灰(我全都要)?

那还是回归最本源的面向对象吧,使用Object作为最稳固的桥梁将数据传递到位后,然后再做类型转换吧。当然,这种工作应该在保证方法安全性的前提下进行,比如List类型的indexOfcontains方法。

有时间了再来具体水水。现在对泛型理解还不够深,先码一下

在写编译原理的时候,在词法分析器类里放了一个ifstream类型的成员变量,然后给语法分析器写一个构造函数,把生成的词法分析器作为参数传入。

声明代码大致如下:

class SimpleLexiconAnalyzer
{
public:
    SimpleLexiconAnalyzer(string filename);
    Word scan();
private:
    ifstream stream;
    char nextChar();
    static ifstream openFile(string filename);
};

class GrammarAnalyzer
{
public:
    GrammarAnalyzer(SimpleLexiconAnalyzer tokenSrc);
    void parseRules(vector<Word> tokens);
    static bool equals(vector<Word> a, vector<Word> b);
private:
    SimpleLexiconAnalyzer &tokenSrc;
    map<Word, vector<WordSeq>> rules;
};

然后就碰到了标题所述的问题。发现问题出在

    GrammarAnalyzer(SimpleLexiconAnalyzer tokenSrc);

这一行。经过资料查阅后,发现在C++中stream类型的对象是不允许被复制的(在类的定义时,删除了operator=操作),决定换用引用。查了老半天,卡在了引用的初始化上(根据C++语法,引用变量是不允许重新赋值的),然后在stackoverflow上受到了一点启发,写出了如下代码:

GrammarAnalyzer::GrammarAnalyzer(SimpleLexiconAnalyzer &tokenSrc): tokenSrc(tokenSrc) {}

然后错误消失,编译顺利通过。

顺带一提,启发来自一年前写的数据结构代码。鬼知道为什么当时我能查到可以这么写,结果一年后不但全忘了,连查都快查不到了。。。

在Eclipse中使用Gradle配置Web项目的时候,默认包含了junit测试工具,但是打开Project and External Dependencies的时候,发现junit-4.12.jar是灰色的,可能由于配置的原因,一开始在Eclipse里面甚至提示找不到junit包。

后来在网上查阅资料发现灰色表示引用了库,但是不参与编译,即对于java编译器来说是找不到该库的。显然找不到的类Eclipse是不会给提示的,但写个java没代码补全这日子咋过。

于是打算用各种方法,包括在build.gradle里把scope改成implementation,在Eclipse的build path里添加自带的junit库,总而言之要想办法把红线去掉,顺便把代码提示搞出来。<-这些都是错误示例。

最后重启了一下Eclipse就发现好了。

最近在学操作系统的时候,接触到了Linux的部分基础函数,比如与进程相关的forkexec等。其中exec是一个系列的函数,有一个名字相似的函数族,比如execvexecl等等。根据之前看到的某篇博客,叙述了exec后跟不同字母的含义:

l(可能代指list?)表示参数可以通过C/C++函数调用时的可变参数的形式调用exec,只需最后传递一个NULL表示参数结束即可; v(可能代指variable?)表示参数以一个字符串数组的形式将参数传递给待调用进程,同样要求最有一个元素为NULLp表示允许带PATH的相对路径搜索; e表示传递环境变量。

其中这个execvp,在MSVC中的thread.h中的定义是这样的:

_DCRTIMP intptr_t __cdecl execvp(
    _In_z_ char const*        _FileName,
    _In_z_ char const* const* _Arguments
    );

然后就看到了这么一个神奇的参数类型定义:char const* const*,在IntelliCode里给出的是另一个定义:const char* const*。这两者一样吗?分别代表的又是什么类型的数据?

然后就涉及到了最基本的问题,const修饰符与变量类型申明关键字,以及指针之间的关系。

两个在网上随便找找就能见到的例子:

char* const s1 = ...;
const char* s2 = ...;

两者分别定义了什么?有区别吗?

答案是,两者都定义了一个char*类型的指针,都具有某些值不能被修改的限制(const修饰符)。但是,第一个限制的是,s1本身不能被修改,而第二个限制的是不允许通过s2修改*s2的值。换句话说,可以对s1进行的相关写操作是修改*s1的内容,而可以对s2进行的相关写操作是修改s2本身。相应地,对s2不要求初始化,而s1一定要初始化,否则会报错,因为后续就不允许修改了,而使用没有初始化过的变量,结果又是不可预测的。

其中对声明的性质起到关键作用的是指针符号*的位置。

来自StackOverflow的一个答案:

int       *       mutable_pointer_to_mutable_int;
int const *       mutable_pointer_to_constant_int;
int       * const constant_pointer_to_mutable_int;
int const * const constant_pointer_to_constant_int;

从这几个声明可以看出,若是*const前面,则指针本身不可修改;否则指向的数据不可修改。可以结合指针的使用进行理解:定义的时候声明了const *p,则p可修改,*p不可修改;若是*const p,则*p可修改,p不可修改,此时const的作用域仅仅在p上。若是在*前后都加上const,则两者的作用复合,形成一个无论如何都无法修改的指针。

此外,由于const和数据类型的声明符的顺序没有影响,所以const int*int const*的作用是一样的。

然后就回到了char const* const*const char* const*的关系上。显然,由于char和第一个const在所有*的同侧,交换顺序不影响定义,所以两者是等价的。

那它定义了个啥呢?

首先去除所有的const,因为它仅仅是对于读写属性的限制,则实际的类型定义为:char **,也就是一个char类型的二级指针,即char*指针的指针。也就是说,定义的东西是一个传统的字符串数组,但是由于是使用指针而不是数组定义的,所以每个一维串的长度可以不一致,整个字符串数组的内存分配也不一定连续。然后再补回const属性,假设指针名为p,则该声明限定了*p**p是常量,不允许通过该指针进行修改。也就是说,char const* const* p定义了一个char类型的二级指针,且不允许修改该指针指向地址的值,这个值是一个直接指向char类型数据的指针;也不允许修改这个值作为指针,所指向的目标内存地址的值。所以重新把视角拉高,整个声明到底定义了啥?一个指向不允许内容被修改的字符串数组。从功能上看,也就像是Java编程时在某些地方声明的final对象一样,用于确保在执行该函数过程时,数据不会被有意或无意地修改。